diff --git a/examples/example.v b/examples/example.v
new file mode 100644
index 0000000..720ffba
--- /dev/null
+++ b/examples/example.v
@@ -0,0 +1,274 @@
+module main
+
+import zinit
+
+
+// Example 1: Get the OpenRPC API specification
+fn example_rpc_discover(mut client zinit.Client) {
+ println('1. Getting API specification...')
+
+ spec := client.rpc_discover() or {
+ eprintln('Error getting API spec: ${err}')
+ return
+ }
+
+ println('API Title: ${spec['info'] or { map[string]interface{}{} }['title'] or { 'Unknown' }}')
+ println('API Version: ${spec['info'] or { map[string]interface{}{} }['version'] or { 'Unknown' }}')
+ println()
+}
+
+// Example 2: List all services
+fn example_service_list(mut client zinit.Client) {
+ println('2. Listing all services...')
+
+ services := client.service_list() or {
+ eprintln('Error listing services: ${err}')
+ return
+ }
+
+ if services.len == 0 {
+ println('No services found.')
+ } else {
+ for name, state in services {
+ println(' ${name}: ${state}')
+ }
+ }
+ println()
+}
+
+// Example 3: Get detailed status for a specific service
+fn example_service_status(mut client zinit.Client) {
+ println('3. Getting service status...')
+
+ // Try to get status for a service (replace 'redis' with an actual service name)
+ service_name := 'redis'
+
+ status := client.service_status(service_name) or {
+ eprintln('Error getting service status: ${err}')
+ return
+ }
+
+ println('Service: ${status.name}')
+ println(' State: ${status.state}')
+ println(' Target: ${status.target}')
+ println(' PID: ${status.pid}')
+
+ if status.after.len > 0 {
+ println(' Dependencies:')
+ for dep_name, dep_state in status.after {
+ println(' ${dep_name}: ${dep_state}')
+ }
+ }
+ println()
+}
+
+// Example 4: Create a new service configuration
+fn example_service_create(mut client zinit.Client) {
+ println('4. Creating a new service...')
+
+ // Create a simple service configuration
+ config := zinit.ServiceConfig{
+ exec: '/usr/bin/echo "Hello from my service"'
+ oneshot: true
+ log: 'stdout'
+ env: {
+ 'MY_VAR': 'my_value'
+ 'PATH': '/usr/bin:/bin'
+ }
+ shutdown_timeout: 30
+ }
+
+ service_name := 'example_service'
+
+ result := client.service_create(service_name, config) or {
+ eprintln('Error creating service: ${err}')
+ return
+ }
+
+ println('Service created: ${result}')
+ println()
+}
+
+// Example 5: Service management operations
+fn example_service_management(mut client zinit.Client) {
+ println('5. Service management operations...')
+
+ service_name := 'example_service'
+
+ // Monitor the service
+ client.service_monitor(service_name) or {
+ eprintln('Error monitoring service: ${err}')
+ return
+ }
+ println('Service ${service_name} is now being monitored')
+
+ // Start the service
+ client.service_start(service_name) or {
+ eprintln('Error starting service: ${err}')
+ return
+ }
+ println('Service ${service_name} started')
+
+ // Wait a moment for the service to run (since it's oneshot)
+ // In a real application, you might want to check the status instead
+
+ // Stop the service (if it's still running)
+ client.service_stop(service_name) or {
+ // This might fail if the service already finished (oneshot)
+ println('Note: Could not stop service (might have already finished): ${err}')
+ }
+
+ // Forget the service (stop monitoring)
+ client.service_forget(service_name) or {
+ eprintln('Error forgetting service: ${err}')
+ return
+ }
+ println('Service ${service_name} is no longer being monitored')
+
+ // Delete the service configuration
+ client.service_delete(service_name) or {
+ eprintln('Error deleting service: ${err}')
+ return
+ }
+ println('Service ${service_name} configuration deleted')
+ println()
+}
+
+// Example 6: Get service statistics
+fn example_service_stats(mut client zinit.Client) {
+ println('6. Getting service statistics...')
+
+ // Try to get stats for a running service
+ service_name := 'redis' // Replace with an actual running service
+
+ stats := client.service_stats(service_name) or {
+ eprintln('Error getting service stats: ${err}')
+ return
+ }
+
+ println('Service: ${stats.name}')
+ println(' PID: ${stats.pid}')
+ println(' Memory Usage: ${stats.memory_usage} bytes (${stats.memory_usage / 1024 / 1024} MB)')
+ println(' CPU Usage: ${stats.cpu_usage}%')
+
+ if stats.children.len > 0 {
+ println(' Child Processes:')
+ for child in stats.children {
+ println(' PID ${child.pid}: ${child.memory_usage} bytes, ${child.cpu_usage}% CPU')
+ }
+ }
+ println()
+}
+
+// Example 7: Log streaming
+fn example_log_streaming(mut client zinit.Client) {
+ println('7. Getting current logs...')
+
+ // Get all current logs
+ logs := client.stream_current_logs(none) or {
+ eprintln('Error getting logs: ${err}')
+ return
+ }
+
+ if logs.len == 0 {
+ println('No logs available')
+ } else {
+ println('Recent logs:')
+ for i, log_entry in logs {
+ println(' ${i + 1}: ${log_entry}')
+ if i >= 4 { // Show only first 5 logs
+ println(' ... (${logs.len - 5} more logs)')
+ break
+ }
+ }
+ }
+
+ // Get logs for a specific service
+ service_logs := client.stream_current_logs('redis') or {
+ eprintln('Error getting service logs: ${err}')
+ return
+ }
+
+ if service_logs.len > 0 {
+ println('Redis service logs:')
+ for log_entry in service_logs {
+ println(' ${log_entry}')
+ }
+ }
+ println()
+}
+
+// Example 8: System operations
+fn example_system_operations(mut client zinit.Client) {
+ println('8. System operations...')
+
+ // Start HTTP server (be careful with this in production)
+ server_result := client.system_start_http_server('127.0.0.1:8080') or {
+ eprintln('Error starting HTTP server: ${err}')
+ return
+ }
+ println('HTTP server: ${server_result}')
+
+ // Stop HTTP server
+ client.system_stop_http_server() or {
+ eprintln('Error stopping HTTP server: ${err}')
+ return
+ }
+ println('HTTP server stopped')
+
+ // Note: system_shutdown() and system_reboot() are commented out
+ // as they would actually shut down or reboot the system!
+
+ // client.system_shutdown() or {
+ // eprintln('Error shutting down system: ${err}')
+ // return
+ // }
+ // println('System shutdown initiated')
+
+ // client.system_reboot() or {
+ // eprintln('Error rebooting system: ${err}')
+ // return
+ // }
+ // println('System reboot initiated')
+
+ println('System operations completed (shutdown/reboot skipped for safety)')
+ println()
+}
+
+
+// Create a new zinit client with default socket path (/tmp/zinit.sock)
+mut client := zinit.new_default_client()
+
+// Alternatively, you can specify a custom socket path:
+// mut client := zinit.new_client('/custom/path/to/zinit.sock')
+
+// Ensure we disconnect when done
+defer {
+ client.disconnect()
+}
+
+println('=== Zinit Client Example ===\n')
+
+// Example 1: Get API specification
+example_rpc_discover(mut client)
+
+// Example 2: List all services
+example_service_list(mut client)
+
+// Example 3: Get service status
+example_service_status(mut client)
+
+// Example 4: Create a new service
+example_service_create(mut client)
+
+// Example 5: Service management operations
+example_service_management(mut client)
+
+// Example 6: Get service statistics
+example_service_stats(mut client)
+
+// Example 7: Log streaming
+example_log_streaming(mut client)
+
+// Example 8: System operations
+example_system_operations(mut client)
diff --git a/herolib b/herolib
new file mode 120000
index 0000000..9f9031a
--- /dev/null
+++ b/herolib
@@ -0,0 +1 @@
+../../../github/freeflowuniverse/herolib
\ No newline at end of file
diff --git a/src/agentui/agentui.v b/src/agentui/agentui.v
new file mode 100644
index 0000000..7107f79
--- /dev/null
+++ b/src/agentui/agentui.v
@@ -0,0 +1,205 @@
+module main
+
+import veb
+import json
+
+// Context struct must embed veb.Context
+pub struct Context {
+ veb.Context
+pub mut:
+ user_id string
+}
+
+// App struct for shared data
+pub struct App {
+pub mut:
+ process_controller ProcessController
+ job_controller JobController
+ system_controller SystemController
+ openrpc_controller OpenRPCController
+}
+
+// Main function
+fn main() {
+ mut app := &App{
+ process_controller: ProcessController{}
+ job_controller: JobController{}
+ system_controller: SystemController{}
+ openrpc_controller: OpenRPCController{}
+ }
+
+ // Use veb.run with App and Context types
+ veb.run[App, Context](mut app, 8082)
+}
+
+// Middleware to run before each request
+pub fn (mut ctx Context) before_request() bool {
+ ctx.user_id = ctx.get_cookie('id') or { '0' }
+ return true
+}
+
+// Index endpoint - Dashboard overview
+@['/']
+pub fn (app &App) index(mut ctx Context) veb.Result {
+ stats := app.system_controller.get_system_stats()
+ processes := app.process_controller.get_all_processes()
+ jobs := app.job_controller.get_all_jobs()
+ return $veb.html()
+}
+
+// Processes endpoint
+@['/processes']
+pub fn (app &App) processes(mut ctx Context) veb.Result {
+ processes := app.process_controller.get_all_processes()
+ return $veb.html()
+}
+
+// Process details endpoint
+@['/processes/:pid']
+pub fn (app &App) process_details(mut ctx Context, pid string) veb.Result {
+ pid_int := pid.int()
+ process := app.process_controller.get_process_by_pid(pid_int) or {
+ return ctx.text('Process not found')
+ }
+ return $veb.html()
+}
+
+// Jobs endpoint
+@['/jobs']
+pub fn (app &App) jobs(mut ctx Context) veb.Result {
+ jobs := app.job_controller.get_all_jobs()
+ return $veb.html()
+}
+
+// Job details endpoint
+@['/jobs/:id']
+pub fn (app &App) job_details(mut ctx Context, id string) veb.Result {
+ id_int := id.u32()
+ job := app.job_controller.get_job_by_id(id_int) or {
+ return ctx.text('Job not found')
+ }
+ return $veb.html()
+}
+
+// OpenRPC endpoint
+@['/openrpc']
+pub fn (app &App) openrpc(mut ctx Context) veb.Result {
+ specs := app.openrpc_controller.get_all_specs()
+ return $veb.html()
+}
+
+// OpenRPC spec details endpoint
+@['/openrpc/:name']
+pub fn (app &App) openrpc_spec(mut ctx Context, name string) veb.Result {
+ spec := app.openrpc_controller.get_spec_by_name(name) or {
+ return ctx.text('OpenRPC specification not found')
+ }
+ methods := app.openrpc_controller.get_methods_for_spec(name)
+ return $veb.html()
+}
+
+// API endpoints
+
+// API endpoint to get all processes
+@['/api/processes'; get]
+pub fn (app &App) api_processes(mut ctx Context) veb.Result {
+ processes := app.process_controller.get_all_processes()
+ json_result := json.encode(processes)
+ return ctx.json(json_result)
+}
+
+// API endpoint to get process by PID
+@['/api/processes/:pid'; get]
+pub fn (app &App) api_process_by_pid(mut ctx Context, pid string) veb.Result {
+ pid_int := pid.int()
+ process := app.process_controller.get_process_by_pid(pid_int) or {
+ return ctx.text('Process not found')
+ }
+ json_result := json.encode(process)
+ return ctx.json(json_result)
+}
+
+// API endpoint to kill a process
+@['/api/processes/:pid/kill'; post]
+pub fn (app &App) api_kill_process(mut ctx Context, pid string) veb.Result {
+ pid_int := pid.int()
+ success := app.process_controller.kill_process(pid_int)
+ return ctx.json('{"success": $success}')
+}
+
+// API endpoint to get all jobs
+@['/api/jobs'; get]
+pub fn (app &App) api_jobs(mut ctx Context) veb.Result {
+ jobs := app.job_controller.get_all_jobs()
+ json_result := json.encode(jobs)
+ return ctx.json(json_result)
+}
+
+// API endpoint to get job by ID
+@['/api/jobs/:id'; get]
+pub fn (app &App) api_job_by_id(mut ctx Context, id string) veb.Result {
+ id_int := id.u32()
+ job := app.job_controller.get_job_by_id(id_int) or {
+ return ctx.text('Job not found')
+ }
+ json_result := json.encode(job)
+ return ctx.json(json_result)
+}
+
+// API endpoint to get jobs by status
+@['/api/jobs/status/:status'; get]
+pub fn (app &App) api_jobs_by_status(mut ctx Context, status string) veb.Result {
+ status_enum := match status {
+ 'new' { JobStatus.new }
+ 'active' { JobStatus.active }
+ 'done' { JobStatus.done }
+ 'error' { JobStatus.error }
+ else { JobStatus.new }
+ }
+
+ jobs := app.job_controller.get_jobs_by_status(status_enum)
+ json_result := json.encode(jobs)
+ return ctx.json(json_result)
+}
+
+// API endpoint to get jobs by circle
+@['/api/jobs/circle/:id'; get]
+pub fn (app &App) api_jobs_by_circle(mut ctx Context, id string) veb.Result {
+ jobs := app.job_controller.get_jobs_by_circle(id)
+ json_result := json.encode(jobs)
+ return ctx.json(json_result)
+}
+
+// API endpoint to get system stats
+@['/api/stats'; get]
+pub fn (app &App) api_stats(mut ctx Context) veb.Result {
+ stats := app.system_controller.get_system_stats()
+ json_result := json.encode(stats)
+ return ctx.json(json_result)
+}
+
+// API endpoint to get all OpenRPC specs
+@['/api/openrpc'; get]
+pub fn (app &App) api_openrpc_specs(mut ctx Context) veb.Result {
+ specs := app.openrpc_controller.get_all_specs()
+ json_result := json.encode(specs)
+ return ctx.json(json_result)
+}
+
+// API endpoint to get OpenRPC spec by name
+@['/api/openrpc/:name'; get]
+pub fn (app &App) api_openrpc_spec_by_name(mut ctx Context, name string) veb.Result {
+ spec := app.openrpc_controller.get_spec_by_name(name) or {
+ return ctx.text('OpenRPC specification not found')
+ }
+ json_result := json.encode(spec)
+ return ctx.json(json_result)
+}
+
+// API endpoint to execute an RPC call
+@['/api/openrpc/:name/:method'; post]
+pub fn (app &App) api_execute_rpc(mut ctx Context, name string, method string) veb.Result {
+ params := ctx.req.body
+ result := app.openrpc_controller.execute_rpc(name, method, params)
+ return ctx.json(result)
+}
\ No newline at end of file
diff --git a/src/agentui/controllers/job_ctrl.v b/src/agentui/controllers/job_ctrl.v
new file mode 100644
index 0000000..0030f79
--- /dev/null
+++ b/src/agentui/controllers/job_ctrl.v
@@ -0,0 +1,51 @@
+
+// JobController handles job-related operations
+pub struct JobController {
+pub:
+ // Will add dependencies here when connecting to openrpc
+}
+
+// get_all_jobs returns all jobs in the system
+pub fn (jc &JobController) get_all_jobs() []JobInfo {
+ // For now using fake data, will be replaced with openrpc calls
+ return get_all_jobs()
+}
+
+// get_job_by_id returns a specific job by ID
+pub fn (jc &JobController) get_job_by_id(id u32) ?JobInfo {
+ jobs := get_all_jobs()
+ for job in jobs {
+ if job.job_id == id {
+ return job
+ }
+ }
+ return error('Job not found')
+}
+
+// get_jobs_by_status returns jobs with a specific status
+pub fn (jc &JobController) get_jobs_by_status(status JobStatus) []JobInfo {
+ jobs := get_all_jobs()
+ mut filtered_jobs := []JobInfo{}
+
+ for job in jobs {
+ if job.status == status {
+ filtered_jobs << job
+ }
+ }
+
+ return filtered_jobs
+}
+
+// get_jobs_by_circle returns jobs for a specific circle
+pub fn (jc &JobController) get_jobs_by_circle(circle_id string) []JobInfo {
+ jobs := get_all_jobs()
+ mut filtered_jobs := []JobInfo{}
+
+ for job in jobs {
+ if job.circle_id == circle_id {
+ filtered_jobs << job
+ }
+ }
+
+ return filtered_jobs
+}
\ No newline at end of file
diff --git a/src/agentui/controllers/process_ctrl.v b/src/agentui/controllers/process_ctrl.v
new file mode 100644
index 0000000..2288852
--- /dev/null
+++ b/src/agentui/controllers/process_ctrl.v
@@ -0,0 +1,31 @@
+module controllers
+
+// ProcessController handles process-related operations
+pub struct ProcessController {
+pub:
+ // Will add dependencies here when connecting to openrpc
+}
+
+// get_all_processes returns all processes in the system
+pub fn (pc &ProcessController) get_all_processes() []Process {
+ // For now using fake data, will be replaced with openrpc calls
+ return get_all_processes()
+}
+
+// get_process_by_pid returns a specific process by PID
+pub fn (pc &ProcessController) get_process_by_pid(pid int) ?Process {
+ processes := get_all_processes()
+ for process in processes {
+ if process.pid == pid {
+ return process
+ }
+ }
+ return error('Process not found')
+}
+
+// kill_process attempts to kill a process
+pub fn (pc &ProcessController) kill_process(pid int) bool {
+ // Fake implementation for now
+ // Will be replaced with actual implementation using openrpc
+ return true
+}
diff --git a/src/agentui/controllers/rpc_ctrl.v b/src/agentui/controllers/rpc_ctrl.v
new file mode 100644
index 0000000..fe3ffad
--- /dev/null
+++ b/src/agentui/controllers/rpc_ctrl.v
@@ -0,0 +1,36 @@
+module controllers
+// OpenRPCController handles OpenRPC-related operations
+pub struct OpenRPCController {
+pub:
+ // Will add dependencies here when connecting to openrpc
+}
+
+// get_all_specs returns all OpenRPC specifications
+pub fn (oc &OpenRPCController) get_all_specs() []OpenRPCSpec {
+ // For now using fake data, will be replaced with openrpc calls
+ return get_all_openrpc_specs()
+}
+
+// get_spec_by_name returns a specific OpenRPC specification by name
+pub fn (oc &OpenRPCController) get_spec_by_name(name string) ?OpenRPCSpec {
+ specs := get_all_openrpc_specs()
+ for spec in specs {
+ if spec.title.to_lower() == name.to_lower() {
+ return spec
+ }
+ }
+ return error('OpenRPC specification not found')
+}
+
+// get_methods_for_spec returns all methods for a specific OpenRPC specification
+pub fn (oc &OpenRPCController) get_methods_for_spec(spec_name string) []OpenRPCMethod {
+ spec := oc.get_spec_by_name(spec_name) or { return [] }
+ return spec.methods
+}
+
+// execute_rpc executes an RPC call
+pub fn (oc &OpenRPCController) execute_rpc(spec_name string, method_name string, params string) string {
+ // Fake implementation for now
+ // Will be replaced with actual implementation using openrpc
+ return '{"result": "Success", "data": {"id": 123, "name": "Test Result"}}'
+}
\ No newline at end of file
diff --git a/src/agentui/controllers/system_controller.v b/src/agentui/controllers/system_controller.v
new file mode 100644
index 0000000..0e52c3e
--- /dev/null
+++ b/src/agentui/controllers/system_controller.v
@@ -0,0 +1,13 @@
+module controllers
+
+// SystemController handles system-related operations
+pub struct SystemController {
+pub:
+ // Will add dependencies here when connecting to openrpc
+}
+
+// get_system_stats returns the current system statistics
+pub fn (sc &SystemController) get_system_stats() SystemStats {
+ // For now using fake data, will be replaced with openrpc calls
+ return get_system_stats()
+}
diff --git a/src/agentui/models/jobs.v b/src/agentui/models/jobs.v
new file mode 100644
index 0000000..01aa659
--- /dev/null
+++ b/src/agentui/models/jobs.v
@@ -0,0 +1,97 @@
+module main
+
+import time
+
+// JobStatus represents the status of a job
+pub enum JobStatus {
+ new
+ active
+ done
+ error
+}
+
+// ParamsType represents the type of parameters for a job
+pub enum ParamsType {
+ hero_script
+ open_rpc
+ rhai_script
+ ai
+}
+
+// JobInfo represents job information for the UI
+pub struct JobInfo {
+pub:
+ job_id u32
+ session_key string
+ circle_id string
+ topic string
+ params_type ParamsType
+ status JobStatus
+ time_scheduled time.Time
+ time_start time.Time
+ time_end time.Time
+ duration string
+ error string
+ has_error bool
+}
+
+
+// get_all_jobs returns a list of mock jobs
+pub fn get_all_jobs() []JobInfo {
+ now := time.now()
+
+ return [
+ JobInfo{
+ job_id: 1
+ circle_id: 'circle1'
+ topic: 'email'
+ params_type: .hero_script
+ status: .done
+ time_scheduled: now.add(-30 * time.minute)
+ time_start: now.add(-29 * time.minute)
+ time_end: now.add(-28 * time.minute)
+ duration: '1m0s'
+ error: ''
+ has_error: false
+ },
+ JobInfo{
+ job_id: 2
+ circle_id: 'circle1'
+ topic: 'backup'
+ params_type: .open_rpc
+ status: .active
+ time_scheduled: now.add(-15 * time.minute)
+ time_start: now.add(-14 * time.minute)
+ time_end: time.Time{}
+ duration: '14m0s'
+ error: ''
+ has_error: false
+ },
+ JobInfo{
+ job_id: 3
+ circle_id: 'circle2'
+ topic: 'sync'
+ params_type: .rhai_script
+ status: .error
+ time_scheduled: now.add(-45 * time.minute)
+ time_start: now.add(-44 * time.minute)
+ time_end: now.add(-43 * time.minute)
+ duration: '1m0s'
+ error: 'Failed to connect to remote server'
+ has_error: true
+ },
+ JobInfo{
+ job_id: 4
+ circle_id: 'circle2'
+ topic: 'email'
+ params_type: .hero_script
+ status: .new
+ time_scheduled: now.add(-5 * time.minute)
+ time_start: time.Time{}
+ time_end: time.Time{}
+ duration: 'Not started'
+ error: ''
+ has_error: false
+ }
+ ]
+}
diff --git a/src/agentui/models/openrpc.v b/src/agentui/models/openrpc.v
new file mode 100644
index 0000000..44bca6e
--- /dev/null
+++ b/src/agentui/models/openrpc.v
@@ -0,0 +1,68 @@
+module main
+
+import time
+
+// OpenRPCMethod represents a method in an OpenRPC specification
+pub struct OpenRPCMethod {
+pub:
+ name string
+ description string
+ summary string
+}
+
+// OpenRPCSpec represents an OpenRPC specification
+pub struct OpenRPCSpec {
+pub:
+ title string
+ description string
+ version string
+ methods []OpenRPCMethod
+}
+
+
+// Mock data functions
+
+// get_all_openrpc_specs returns a list of mock OpenRPC specifications
+pub fn get_all_openrpc_specs() []OpenRPCSpec {
+ return [
+ OpenRPCSpec{
+ title: 'Process Manager'
+ description: 'API for managing system processes'
+ version: '1.0.0'
+ methods: [
+ OpenRPCMethod{
+ name: 'get_processes'
+ description: 'Get a list of all processes'
+ summary: 'List processes'
+ },
+ OpenRPCMethod{
+ name: 'kill_process'
+ description: 'Kill a process by PID'
+ summary: 'Kill process'
+ }
+ ]
+ },
+ OpenRPCSpec{
+ title: 'Job Manager'
+ description: 'API for managing jobs'
+ version: '1.0.0'
+ methods: [
+ OpenRPCMethod{
+ name: 'get_jobs'
+ description: 'Get a list of all jobs'
+ summary: 'List jobs'
+ },
+ OpenRPCMethod{
+ name: 'get_job'
+ description: 'Get a job by ID'
+ summary: 'Get job'
+ },
+ OpenRPCMethod{
+ name: 'create_job'
+ description: 'Create a new job'
+ summary: 'Create job'
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/agentui/models/process.v b/src/agentui/models/process.v
new file mode 100644
index 0000000..eeb486c
--- /dev/null
+++ b/src/agentui/models/process.v
@@ -0,0 +1,50 @@
+module main
+
+
+// Process represents a single running process with its relevant details
+pub struct Process {
+pub:
+ pid int
+ name string // CPU usage percentage
+ memory f64 // Memory usage in MB
+ cpu f64
+}
+
+
+
+// get_all_processes returns a list of mock processes
+pub fn get_all_processes() []Process {
+ // Fake data for now, will be replaced with openrpc calls later
+ return [
+ Process{
+ pid: 1001
+ name: 'SystemIdleProcess'
+ cpu: 95.5
+ memory: 0.1
+ },
+ Process{
+ pid: 1002
+ name: 'explorer.exe'
+ cpu: 1.2
+ memory: 150.7
+ },
+ Process{
+ pid: 1003
+ name: 'chrome.exe'
+ cpu: 25.8
+ memory: 512.3
+ },
+ Process{
+ pid: 1004
+ name: 'code.exe'
+ cpu: 5.1
+ memory: 350.0
+ },
+ Process{
+ pid: 1005
+ name: 'v.exe'
+ cpu: 0.5
+ memory: 80.2
+ }
+ ]
+}
diff --git a/src/agentui/models/stats.v b/src/agentui/models/stats.v
new file mode 100644
index 0000000..18a3a07
--- /dev/null
+++ b/src/agentui/models/stats.v
@@ -0,0 +1,28 @@
+module main
+
+import time
+
+
+// SystemStats represents system statistics
+pub struct SystemStats {
+pub:
+ cpu_usage f32 [json: 'cpu_usage']
+ memory_usage f32 [json: 'memory_usage']
+ disk_usage f32 [json: 'disk_usage']
+ uptime string [json: 'uptime']
+ process_count int [json: 'process_count']
+ job_count int [json: 'job_count']
+}
+
+// get_system_stats returns mock system statistics
+pub fn get_system_stats() SystemStats {
+ // Fake data for now, will be replaced with openrpc calls later
+ return SystemStats{
+ cpu_usage: 45.2
+ memory_usage: 62.7
+ disk_usage: 38.5
+ uptime: '3 days, 7 hours, 22 minutes'
+ process_count: 87
+ job_count: 4
+ }
+}
\ No newline at end of file
diff --git a/src/agentui/static/css/style.css b/src/agentui/static/css/style.css
new file mode 100644
index 0000000..912ab44
--- /dev/null
+++ b/src/agentui/static/css/style.css
@@ -0,0 +1,728 @@
+/* Base styles */
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ line-height: 1.6;
+ color: #333;
+ background-color: #f4f7f9;
+ padding: 0;
+ margin: 0;
+}
+
+header {
+ background-color: #2c3e50;
+ color: white;
+ padding: 1rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+nav {
+ display: flex;
+ gap: 1rem;
+}
+
+nav a {
+ color: white;
+ text-decoration: none;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ transition: background-color 0.3s;
+}
+
+nav a:hover, nav a.active {
+ background-color: #34495e;
+}
+
+nav a.active {
+ font-weight: bold;
+ background-color: #1a252f;
+}
+
+main {
+ max-width: 1200px;
+ margin: 2rem auto;
+ padding: 0 2rem;
+}
+
+footer {
+ text-align: center;
+ padding: 1rem;
+ background-color: #2c3e50;
+ color: white;
+ margin-top: 2rem;
+}
+
+h1, h2, h3 {
+ margin-bottom: 1rem;
+}
+
+/* Common components */
+.btn {
+ display: inline-block;
+ padding: 0.5rem 1rem;
+ background-color: #3498db;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ text-decoration: none;
+ font-size: 0.9rem;
+ transition: background-color 0.3s;
+}
+
+.btn:hover {
+ background-color: #2980b9;
+}
+
+.btn-small {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.8rem;
+}
+
+.btn-primary {
+ background-color: #3498db;
+}
+
+.btn-primary:hover {
+ background-color: #2980b9;
+}
+
+.btn-warning {
+ background-color: #f39c12;
+}
+
+.btn-warning:hover {
+ background-color: #e67e22;
+}
+
+.btn-danger {
+ background-color: #e74c3c;
+}
+
+.btn-danger:hover {
+ background-color: #c0392b;
+}
+
+.data-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 1rem 0;
+ background-color: white;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+.data-table th, .data-table td {
+ padding: 0.75rem 1rem;
+ text-align: left;
+ border-bottom: 1px solid #ecf0f1;
+}
+
+.data-table th {
+ background-color: #f8f9fa;
+ font-weight: bold;
+ color: #2c3e50;
+}
+
+.data-table tr:hover {
+ background-color: #f8f9fa;
+}
+
+.data-table tr:last-child td {
+ border-bottom: none;
+}
+
+.filter-controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+ background-color: white;
+ padding: 1rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+.filter-controls input[type="text"] {
+ padding: 0.5rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ width: 300px;
+}
+
+.filter-buttons {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.filter-btn {
+ padding: 0.5rem 1rem;
+ background-color: #f8f9fa;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.3s;
+}
+
+.filter-btn:hover {
+ background-color: #e9ecef;
+}
+
+.filter-btn.active {
+ background-color: #3498db;
+ color: white;
+ border-color: #3498db;
+}
+
+.sort-controls {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.sort-controls select {
+ padding: 0.5rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+}
+
+.sort-controls button {
+ padding: 0.5rem;
+ background-color: #f8f9fa;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.3s;
+}
+
+.sort-controls button:hover {
+ background-color: #e9ecef;
+}
+
+.detail-section {
+ margin: 1.5rem 0;
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+}
+
+.detail-item {
+ display: flex;
+ margin-bottom: 1rem;
+ border-bottom: 1px solid #ecf0f1;
+ padding-bottom: 0.5rem;
+}
+
+.detail-item:last-child {
+ border-bottom: none;
+ margin-bottom: 0;
+}
+
+.label {
+ font-weight: bold;
+ width: 150px;
+ color: #7f8c8d;
+}
+
+.value {
+ flex: 1;
+}
+
+.error-item {
+ background-color: #fdedec;
+ border-radius: 4px;
+ padding: 0.5rem;
+}
+
+.error-value {
+ color: #e74c3c;
+}
+
+/* Dashboard styles */
+.dashboard-overview {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.stats-section, .processes-section, .jobs-section {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: 1.5rem;
+}
+
+.stat-card {
+ background-color: #f8f9fa;
+ border-radius: 8px;
+ padding: 1.5rem;
+ text-align: center;
+}
+
+.stat-value {
+ font-size: 2rem;
+ font-weight: bold;
+ color: #2c3e50;
+ margin: 1rem 0;
+}
+
+.stat-bar {
+ height: 10px;
+ background-color: #ecf0f1;
+ border-radius: 5px;
+ overflow: hidden;
+ margin-top: 1rem;
+}
+
+.stat-fill {
+ height: 100%;
+ background-color: #3498db;
+}
+
+.view-all {
+ font-size: 0.9rem;
+ color: #3498db;
+ text-decoration: none;
+ margin-left: 1rem;
+}
+
+.view-all:hover {
+ text-decoration: underline;
+}
+
+/* Process styles */
+.processes-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.process-details-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.process-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+}
+
+.process-actions {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.detail-bar {
+ height: 10px;
+ background-color: #ecf0f1;
+ border-radius: 5px;
+ overflow: hidden;
+ margin-top: 0.5rem;
+}
+
+.detail-fill {
+ height: 100%;
+ background-color: #3498db;
+}
+
+.process-info {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+}
+
+.info-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
+ gap: 1.5rem;
+ margin-top: 1rem;
+}
+
+.info-card {
+ background-color: #f8f9fa;
+ border-radius: 8px;
+ padding: 1.5rem;
+}
+
+.chart-placeholder {
+ height: 200px;
+ background-color: #ecf0f1;
+ border-radius: 8px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 1rem;
+}
+
+.placeholder-text {
+ color: #7f8c8d;
+ font-style: italic;
+}
+
+/* Job styles */
+.jobs-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.job-details-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.job-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+ border-left: 5px solid #3498db;
+}
+
+.job-header.status-new {
+ border-left-color: #3498db;
+}
+
+.job-header.status-active {
+ border-left-color: #2ecc71;
+}
+
+.job-header.status-done {
+ border-left-color: #27ae60;
+}
+
+.job-header.status-error {
+ border-left-color: #e74c3c;
+}
+
+.status-badge {
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ font-weight: bold;
+ text-transform: uppercase;
+}
+
+.status-badge.status-new {
+ background-color: #ebf5fb;
+ color: #3498db;
+}
+
+.status-badge.status-active {
+ background-color: #e9f7ef;
+ color: #2ecc71;
+}
+
+.status-badge.status-done {
+ background-color: #eafaf1;
+ color: #27ae60;
+}
+
+.status-badge.status-error {
+ background-color: #fdedec;
+ color: #e74c3c;
+}
+
+.job-timeline {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+}
+
+.timeline {
+ position: relative;
+ padding: 1rem 0;
+}
+
+.timeline::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 20px;
+ width: 2px;
+ background-color: #ecf0f1;
+}
+
+.timeline-item {
+ position: relative;
+ padding-left: 50px;
+ margin-bottom: 2rem;
+}
+
+.timeline-item:last-child {
+ margin-bottom: 0;
+}
+
+.timeline-icon {
+ position: absolute;
+ left: 11px;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-color: #3498db;
+}
+
+.timeline-icon.scheduled {
+ background-color: #3498db;
+}
+
+.timeline-icon.started {
+ background-color: #2ecc71;
+}
+
+.timeline-icon.completed {
+ background-color: #27ae60;
+}
+
+.timeline-icon.error {
+ background-color: #e74c3c;
+}
+
+.timeline-content {
+ background-color: #f8f9fa;
+ border-radius: 8px;
+ padding: 1rem;
+}
+
+.timeline-content h4 {
+ margin-bottom: 0.5rem;
+}
+
+.error-message {
+ color: #e74c3c;
+ margin-top: 0.5rem;
+ padding: 0.5rem;
+ background-color: #fdedec;
+ border-radius: 4px;
+}
+
+/* OpenRPC styles */
+.openrpc-container {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.specs-list {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+}
+
+.specs-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1.5rem;
+ margin-top: 1rem;
+}
+
+.spec-card {
+ background-color: #f8f9fa;
+ border-radius: 8px;
+ padding: 1.5rem;
+ transition: transform 0.3s, box-shadow 0.3s;
+}
+
+.spec-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+}
+
+.spec-version {
+ font-size: 0.9rem;
+ color: #7f8c8d;
+ margin-bottom: 0.5rem;
+}
+
+.spec-description {
+ margin-bottom: 1rem;
+}
+
+.spec-methods {
+ margin-bottom: 1rem;
+}
+
+.openrpc-info {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+}
+
+.openrpc-info ul {
+ margin-left: 1.5rem;
+ margin-top: 0.5rem;
+}
+
+.openrpc-spec-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.spec-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+}
+
+.spec-meta {
+ font-size: 0.9rem;
+}
+
+.spec-description {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+}
+
+.methods-section {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+}
+
+.methods-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1.5rem;
+ margin-top: 1rem;
+}
+
+.method-card {
+ background-color: #f8f9fa;
+ border-radius: 8px;
+ padding: 1.5rem;
+}
+
+.method-name {
+ color: #2c3e50;
+ margin-bottom: 0.5rem;
+}
+
+.method-summary {
+ font-weight: bold;
+ margin-bottom: 0.5rem;
+}
+
+.method-description {
+ margin-bottom: 1rem;
+ color: #7f8c8d;
+}
+
+.method-executor {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+}
+
+.method-executor.hidden {
+ display: none;
+}
+
+.executor-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+
+.close-btn {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ color: #7f8c8d;
+}
+
+.executor-body {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.params-section, .result-section {
+ background-color: #f8f9fa;
+ border-radius: 8px;
+ padding: 1rem;
+}
+
+#params-editor {
+ width: 100%;
+ height: 150px;
+ font-family: monospace;
+ padding: 0.5rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ resize: vertical;
+}
+
+#result-display {
+ width: 100%;
+ min-height: 150px;
+ background-color: #2c3e50;
+ color: white;
+ padding: 1rem;
+ border-radius: 4px;
+ overflow: auto;
+ font-family: monospace;
+ white-space: pre-wrap;
+}
+
+.executor-actions {
+ display: flex;
+ justify-content: flex-end;
+}
+
+/* Status colors for table rows */
+tr.status-new {
+ border-left: 4px solid #3498db;
+}
+
+tr.status-active {
+ border-left: 4px solid #2ecc71;
+}
+
+tr.status-done {
+ border-left: 4px solid #27ae60;
+}
+
+tr.status-error {
+ border-left: 4px solid #e74c3c;
+}
\ No newline at end of file
diff --git a/src/agentui/static/js/main.js b/src/agentui/static/js/main.js
new file mode 100644
index 0000000..4992f99
--- /dev/null
+++ b/src/agentui/static/js/main.js
@@ -0,0 +1,174 @@
+// Main JavaScript file for Hero Agent UI
+
+document.addEventListener('DOMContentLoaded', function() {
+ console.log('Hero Agent UI loaded');
+
+ // Fetch latest data for the page if needed
+ refreshPageData();
+
+ // Set up auto-refresh for dashboard if on that page
+ if (window.location.pathname === '/') {
+ setInterval(refreshDashboard, 10000); // Refresh every 10 seconds
+ }
+});
+
+// Refresh data based on current page
+function refreshPageData() {
+ const path = window.location.pathname;
+
+ if (path === '/') {
+ refreshDashboard();
+ } else if (path === '/processes') {
+ refreshProcesses();
+ } else if (path.startsWith('/processes/')) {
+ const pid = path.split('/').pop();
+ refreshProcessDetails(pid);
+ } else if (path === '/jobs') {
+ refreshJobs();
+ } else if (path.startsWith('/jobs/')) {
+ const jobId = path.split('/').pop();
+ refreshJobDetails(jobId);
+ } else if (path === '/openrpc') {
+ refreshOpenRPCSpecs();
+ } else if (path.startsWith('/openrpc/')) {
+ const specName = path.split('/').pop();
+ refreshOpenRPCSpecDetails(specName);
+ }
+}
+
+// Fetch and update dashboard data
+function refreshDashboard() {
+ // Fetch system stats
+ fetch('/api/stats')
+ .then(response => response.json())
+ .then(data => {
+ console.log('Dashboard stats refreshed');
+ // In a real implementation, we would update the DOM here
+ // For now, this is just a placeholder
+ updateStatBars(data);
+ })
+ .catch(error => {
+ console.error('Error refreshing dashboard stats:', error);
+ });
+
+ // Fetch processes
+ fetch('/api/processes')
+ .then(response => response.json())
+ .then(data => {
+ console.log('Dashboard processes refreshed');
+ // In a real implementation, we would update the DOM here
+ })
+ .catch(error => {
+ console.error('Error refreshing dashboard processes:', error);
+ });
+
+ // Fetch jobs
+ fetch('/api/jobs')
+ .then(response => response.json())
+ .then(data => {
+ console.log('Dashboard jobs refreshed');
+ // In a real implementation, we would update the DOM here
+ })
+ .catch(error => {
+ console.error('Error refreshing dashboard jobs:', error);
+ });
+}
+
+// Update stat bars with new data
+function updateStatBars(stats) {
+ // This is a placeholder function that would update the stat bars
+ // with the new data from the API
+ if (!stats) return;
+
+ const cpuBar = document.querySelector('.stat-fill[data-stat="cpu"]');
+ const memoryBar = document.querySelector('.stat-fill[data-stat="memory"]');
+ const diskBar = document.querySelector('.stat-fill[data-stat="disk"]');
+
+ if (cpuBar) cpuBar.style.width = `${stats.cpu_usage}%`;
+ if (memoryBar) memoryBar.style.width = `${stats.memory_usage}%`;
+ if (diskBar) diskBar.style.width = `${stats.disk_usage}%`;
+}
+
+// Fetch and update processes list
+function refreshProcesses() {
+ fetch('/api/processes')
+ .then(response => response.json())
+ .then(data => {
+ console.log('Processes refreshed');
+ // In a real implementation, we would update the DOM here
+ })
+ .catch(error => {
+ console.error('Error refreshing processes:', error);
+ });
+}
+
+// Fetch and update process details
+function refreshProcessDetails(pid) {
+ fetch(`/api/processes/${pid}`)
+ .then(response => response.json())
+ .then(data => {
+ console.log('Process details refreshed');
+ // In a real implementation, we would update the DOM here
+
+ // Update CPU usage bar
+ const cpuBar = document.querySelector('.detail-fill');
+ if (cpuBar && data.cpu) {
+ cpuBar.style.width = `${data.cpu}%`;
+ }
+ })
+ .catch(error => {
+ console.error('Error refreshing process details:', error);
+ });
+}
+
+// Fetch and update jobs list
+function refreshJobs() {
+ fetch('/api/jobs')
+ .then(response => response.json())
+ .then(data => {
+ console.log('Jobs refreshed');
+ // In a real implementation, we would update the DOM here
+ })
+ .catch(error => {
+ console.error('Error refreshing jobs:', error);
+ });
+}
+
+// Fetch and update job details
+function refreshJobDetails(jobId) {
+ fetch(`/api/jobs/${jobId}`)
+ .then(response => response.json())
+ .then(data => {
+ console.log('Job details refreshed');
+ // In a real implementation, we would update the DOM here
+ })
+ .catch(error => {
+ console.error('Error refreshing job details:', error);
+ });
+}
+
+// Fetch and update OpenRPC specs
+function refreshOpenRPCSpecs() {
+ fetch('/api/openrpc')
+ .then(response => response.json())
+ .then(data => {
+ console.log('OpenRPC specs refreshed');
+ // In a real implementation, we would update the DOM here
+ })
+ .catch(error => {
+ console.error('Error refreshing OpenRPC specs:', error);
+ });
+}
+
+// Fetch and update OpenRPC spec details
+function refreshOpenRPCSpecDetails(specName) {
+ fetch(`/api/openrpc/${specName}`)
+ .then(response => response.json())
+ .then(data => {
+ console.log('OpenRPC spec details refreshed');
+ // In a real implementation, we would update the DOM here
+ })
+ .catch(error => {
+ console.error('Error refreshing OpenRPC spec details:', error);
+ });
+}
\ No newline at end of file
diff --git a/src/agentui/templates/agent_details.html b/src/agentui/templates/agent_details.html
new file mode 100644
index 0000000..67125b4
--- /dev/null
+++ b/src/agentui/templates/agent_details.html
@@ -0,0 +1,100 @@
+
+
+ Agent Details - Hero Agent UI
+
+
+
+
+
+
+
+
@agent.name
+
+
+
+ ID:
+ @agent.id
+
+
+
+ Type:
+ @agent.type_
+
+
+
+ Status:
+ @agent.status
+
+
+
+ Created:
+ @agent.created_at
+
+
+
+ Last Active:
+ @agent.last_active
+
+
+
+
+ Start
+ Restart
+ Stop
+
+
+
+
+
+ © 2025 Hero Agent System
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/agentui/templates/dashboard.html b/src/agentui/templates/dashboard.html
new file mode 100644
index 0000000..9148fcb
--- /dev/null
+++ b/src/agentui/templates/dashboard.html
@@ -0,0 +1,59 @@
+
+
+ Dashboard - Hero Agent UI
+
+
+
+
+
+
+
+
+
CPU Usage
+
@stats.cpu_usage%
+
+
+
+
+
Memory Usage
+
@stats.memory_usage%
+
+
+
+
+
Disk Usage
+
@stats.disk_usage%
+
+
+
+
+
Uptime
+
@stats.uptime
+
+
+
+
Agent Count
+
@stats.agent_count
+
+
+
+
+
+ © 2025 Hero Agent System
+
+
+
+
+
\ No newline at end of file
diff --git a/src/agentui/templates/index.html b/src/agentui/templates/index.html
new file mode 100644
index 0000000..98213d5
--- /dev/null
+++ b/src/agentui/templates/index.html
@@ -0,0 +1,134 @@
+
+
+ Hero Agent UI - Dashboard
+
+
+
+
+
+
+
+
+
System Statistics
+
+
+
CPU Usage
+
@stats.cpu_usage%
+
+
+
+
+
Memory Usage
+
@stats.memory_usage%
+
+
+
+
+
Disk Usage
+
@stats.disk_usage%
+
+
+
+
+
Uptime
+
@stats.uptime
+
+
+
+
+
+
+
+
+
+ PID
+ Name
+ CPU %
+ Memory (MB)
+ Actions
+
+
+
+ @for process in processes
+
+ @process.pid
+ @process.name
+ @process.cpu%
+ @process.memory
+
+ Details
+ Kill
+
+
+ @end
+
+
+
+
+
+
+
+
+
+ ID
+ Topic
+ Status
+ Duration
+ Actions
+
+
+
+ @for job in jobs
+
+ @job.job_id
+ @job.topic
+ @job.status
+ @job.duration
+
+ Details
+
+
+ @end
+
+
+
+
+
+
+
+ © 2025 Hero Agent System
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/agentui/templates/job_details.html b/src/agentui/templates/job_details.html
new file mode 100644
index 0000000..19985ac
--- /dev/null
+++ b/src/agentui/templates/job_details.html
@@ -0,0 +1,114 @@
+
+
+ Hero Agent UI - Job Details
+
+
+
+
+
+
+
+
+
+
+
+ Circle ID:
+ @job.circle_id
+
+
+
+ Session Key:
+ @job.session_key
+
+
+
+ Parameters Type:
+ @job.params_type
+
+
+
+ Scheduled Time:
+ @job.time_scheduled
+
+
+
+ Start Time:
+ @job.time_start
+
+
+
+ End Time:
+ @job.time_end
+
+
+
+ Duration:
+ @job.duration
+
+
+ @if job.has_error
+
+ Error:
+ @job.error
+
+ @end
+
+
+
+
Job Timeline
+
+
+
+
+
Scheduled
+
@job.time_scheduled
+
+
+
+ @if !job.time_start.str().is_blank()
+
+
+
+
Started
+
@job.time_start
+
+
+ @end
+
+ @if !job.time_end.str().is_blank()
+
+
+
+
@if job.has_error { 'Failed' } else { 'Completed' }
+
@job.time_end
+ @if job.has_error
+
@job.error
+ @end
+
+
+ @end
+
+
+
+
+
+
+ © 2025 Hero Agent System
+
+
+
+
+
\ No newline at end of file
diff --git a/src/agentui/templates/jobs.html b/src/agentui/templates/jobs.html
new file mode 100644
index 0000000..0cce795
--- /dev/null
+++ b/src/agentui/templates/jobs.html
@@ -0,0 +1,117 @@
+
+
+ Hero Agent UI - Jobs
+
+
+
+
+
+
+
+
+
+
+ All
+ New
+ Active
+ Done
+ Error
+
+
+
+
+
+
+ ID
+ Circle
+ Topic
+ Status
+ Scheduled
+ Duration
+ Actions
+
+
+
+ @for job in jobs
+
+ @job.job_id
+ @job.circle_id
+ @job.topic
+ @job.status
+ @job.time_scheduled
+ @job.duration
+
+ Details
+
+
+ @end
+
+
+
+
+
+
+ © 2025 Hero Agent System
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/agentui/templates/openrpc.html b/src/agentui/templates/openrpc.html
new file mode 100644
index 0000000..07f5319
--- /dev/null
+++ b/src/agentui/templates/openrpc.html
@@ -0,0 +1,63 @@
+
+
+ Hero Agent UI - OpenRPC
+
+
+
+
+
+
+
+
+
Available Specifications
+
+ @for spec in specs
+
+
@spec.title
+
Version: @spec.version
+
@spec.description
+
@spec.methods.len methods available
+
View Details
+
+ @end
+
+
+
+
+
About OpenRPC
+
+ OpenRPC is a standard for defining JSON-RPC 2.0 APIs. It provides a machine-readable
+ specification format that allows for automatic documentation generation, client SDK
+ generation, and server implementation validation.
+
+
+ The Hero Agent system uses OpenRPC to define and document its APIs, making it easier
+ to interact with the system programmatically.
+
+
Key Benefits
+
+ Standardized API definitions
+ Automatic documentation generation
+ Client SDK generation
+ Server implementation validation
+ Improved developer experience
+
+
+
+
+
+
+ © 2025 Hero Agent System
+
+
+
+
+
\ No newline at end of file
diff --git a/src/agentui/templates/openrpc_spec.html b/src/agentui/templates/openrpc_spec.html
new file mode 100644
index 0000000..7a2b756
--- /dev/null
+++ b/src/agentui/templates/openrpc_spec.html
@@ -0,0 +1,121 @@
+
+
+ Hero Agent UI - OpenRPC Spec
+
+
+
+
+
+
+
+
+
+
+
+
+
Available Methods
+
+ @for method in methods
+
+
@method.name
+
@method.summary
+
@method.description
+
Execute Method
+
+ @end
+
+
+
+
+
Method Executor
+
+
+
+
+
+
Parameters
+
+
+
+
+ Execute
+
+
+
+
Result
+
Results will appear here...
+
+
+
+
+
+
+
+
+ © 2025 Hero Agent System
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/agentui/templates/process_details.html b/src/agentui/templates/process_details.html
new file mode 100644
index 0000000..39a8c8f
--- /dev/null
+++ b/src/agentui/templates/process_details.html
@@ -0,0 +1,91 @@
+
+
+ Hero Agent UI - Process Details
+
+
+
+
+
+
+
+
+
+
+
+ PID:
+ @process.pid
+
+
+
+
CPU Usage:
+
@process.cpu%
+
+
+
+
+ Memory Usage:
+ @process.memory MB
+
+
+
+
+
Process Information
+
+
+
CPU Usage Over Time
+
+
+
CPU usage chart would be displayed here
+
+
+
+
+
Memory Usage Over Time
+
+
+
Memory usage chart would be displayed here
+
+
+
+
+
+
+
+
+ © 2025 Hero Agent System
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/agentui/templates/processes.html b/src/agentui/templates/processes.html
new file mode 100644
index 0000000..ee72b9e
--- /dev/null
+++ b/src/agentui/templates/processes.html
@@ -0,0 +1,159 @@
+
+
+ Hero Agent UI - Processes
+
+
+
+
+
+
+
+
+
+
+ Sort by:
+
+ PID
+ Name
+ CPU Usage
+ Memory Usage
+
+ ↑
+
+
+
+
+
+
+ PID
+ Name
+ CPU %
+ Memory (MB)
+ Actions
+
+
+
+ @for process in processes
+
+ @process.pid
+ @process.name
+ @process.cpu
+ @process.memory
+
+ Details
+ Kill
+
+
+ @end
+
+
+
+
+
+
+ © 2025 Hero Agent System
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/clients/zinit/README.md b/src/clients/zinit/README.md
new file mode 100644
index 0000000..2aee592
--- /dev/null
+++ b/src/clients/zinit/README.md
@@ -0,0 +1,406 @@
+# Zinit Client Module
+
+A well-documented V (Vlang) client library for interacting with the Zinit service manager via JSON-RPC over Unix socket.
+
+## Overview
+
+This module provides a complete client implementation for the Zinit JSON-RPC API, allowing you to manage services, monitor system resources, and control the Zinit service manager programmatically.
+
+## Features
+
+- **Service Management**: Start, stop, monitor, and forget services
+- **Service Configuration**: Create, delete, and retrieve service configurations
+- **System Control**: Shutdown, reboot, and HTTP server management
+- **Monitoring**: Get service status, statistics, and resource usage
+- **Logging**: Stream current logs and subscribe to log updates
+- **Error Handling**: Comprehensive error handling with detailed error messages
+- **Type Safety**: Strongly typed structs for all API responses
+- **Connection Management**: Automatic connection handling with proper cleanup
+
+## Installation
+
+Simply import the module in your V project:
+
+```v
+import zinit
+```
+
+## Quick Start
+
+```v
+import zinit
+
+fn main() {
+ // Create a client with default socket path (/tmp/zinit.sock)
+ mut client := zinit.new_default_client()
+
+ // Or specify a custom socket path
+ // mut client := zinit.new_client('/custom/path/to/zinit.sock')
+
+ defer {
+ client.disconnect()
+ }
+
+ // List all services
+ services := client.service_list() or {
+ eprintln('Error: ${err}')
+ return
+ }
+
+ for name, state in services {
+ println('${name}: ${state}')
+ }
+}
+```
+
+## API Reference
+
+### Client Creation
+
+#### `new_client(socket_path string) &Client`
+Creates a new zinit client with a custom socket path.
+
+#### `new_default_client() &Client`
+Creates a new zinit client with the default socket path (`/tmp/zinit.sock`).
+
+### Connection Management
+
+#### `connect() !`
+Establishes a connection to the zinit Unix socket. Called automatically by API methods.
+
+#### `disconnect()`
+Closes the connection to the zinit Unix socket. Should be called when done with the client.
+
+### Service Management
+
+#### `service_list() !map[string]string`
+Lists all services managed by Zinit.
+
+**Returns**: A map of service names to their current states.
+
+```v
+services := client.service_list()!
+for name, state in services {
+ println('${name}: ${state}')
+}
+```
+
+#### `service_status(name string) !ServiceStatus`
+Shows detailed status information for a specific service.
+
+**Parameters**:
+- `name`: The name of the service
+
+**Returns**: `ServiceStatus` struct with detailed information.
+
+```v
+status := client.service_status('redis')!
+println('Service: ${status.name}')
+println('State: ${status.state}')
+println('PID: ${status.pid}')
+```
+
+#### `service_start(name string) !`
+Starts a service.
+
+**Parameters**:
+- `name`: The name of the service to start
+
+#### `service_stop(name string) !`
+Stops a service.
+
+**Parameters**:
+- `name`: The name of the service to stop
+
+#### `service_monitor(name string) !`
+Starts monitoring a service. The service configuration is loaded from the config directory.
+
+**Parameters**:
+- `name`: The name of the service to monitor
+
+#### `service_forget(name string) !`
+Stops monitoring a service. You can only forget a stopped service.
+
+**Parameters**:
+- `name`: The name of the service to forget
+
+#### `service_kill(name string, signal string) !`
+Sends a signal to a running service.
+
+**Parameters**:
+- `name`: The name of the service to send the signal to
+- `signal`: The signal to send (e.g., "SIGTERM", "SIGKILL")
+
+### Service Configuration
+
+#### `service_create(name string, config ServiceConfig) !string`
+Creates a new service configuration file.
+
+**Parameters**:
+- `name`: The name of the service to create
+- `config`: The service configuration
+
+**Returns**: Result message.
+
+```v
+config := zinit.ServiceConfig{
+ exec: '/usr/bin/redis-server'
+ oneshot: false
+ log: 'stdout'
+ env: {
+ 'REDIS_PORT': '6379'
+ }
+ shutdown_timeout: 30
+}
+
+result := client.service_create('redis', config)!
+println('Service created: ${result}')
+```
+
+#### `service_delete(name string) !string`
+Deletes a service configuration file.
+
+**Parameters**:
+- `name`: The name of the service to delete
+
+**Returns**: Result message.
+
+#### `service_get(name string) !ServiceConfig`
+Gets a service configuration file.
+
+**Parameters**:
+- `name`: The name of the service to get
+
+**Returns**: `ServiceConfig` struct with the service configuration.
+
+### Service Statistics
+
+#### `service_stats(name string) !ServiceStats`
+Gets memory and CPU usage statistics for a service.
+
+**Parameters**:
+- `name`: The name of the service to get stats for
+
+**Returns**: `ServiceStats` struct with usage information.
+
+```v
+stats := client.service_stats('redis')!
+println('Memory Usage: ${stats.memory_usage / 1024 / 1024} MB')
+println('CPU Usage: ${stats.cpu_usage}%')
+```
+
+### System Operations
+
+#### `system_shutdown() !`
+Stops all services and powers off the system.
+
+⚠️ **Warning**: This will actually shut down the system!
+
+#### `system_reboot() !`
+Stops all services and reboots the system.
+
+⚠️ **Warning**: This will actually reboot the system!
+
+#### `system_start_http_server(address string) !string`
+Starts an HTTP/RPC server at the specified address.
+
+**Parameters**:
+- `address`: The network address to bind the server to (e.g., "127.0.0.1:8080")
+
+**Returns**: Result message.
+
+#### `system_stop_http_server() !`
+Stops the HTTP/RPC server if running.
+
+### Logging
+
+#### `stream_current_logs(name ?string) ![]string`
+Gets current logs from zinit and monitored services.
+
+**Parameters**:
+- `name`: Optional service name filter. If provided, only logs from this service will be returned.
+
+**Returns**: Array of log strings.
+
+```v
+// Get all logs
+all_logs := client.stream_current_logs(none)!
+
+// Get logs for a specific service
+redis_logs := client.stream_current_logs('redis')!
+```
+
+#### `stream_subscribe_logs(name ?string) !string`
+Subscribes to log messages generated by zinit and monitored services.
+
+**Parameters**:
+- `name`: Optional service name filter.
+
+**Returns**: A single log message.
+
+**Note**: For continuous streaming, call this method repeatedly.
+
+### API Discovery
+
+#### `rpc_discover() !map[string]interface{}`
+Returns the OpenRPC specification for the API.
+
+**Returns**: The complete OpenRPC specification as a map.
+
+## Data Types
+
+### ServiceConfig
+Represents the configuration for a zinit service.
+
+```v
+struct ServiceConfig {
+pub mut:
+ exec string // Command to run
+ oneshot bool // Whether the service should be restarted
+ after []string // Services that must be running before this one starts
+ log string // How to handle service output (null, ring, stdout)
+ env map[string]string // Environment variables for the service
+ shutdown_timeout int // Maximum time to wait for service to stop during shutdown
+}
+```
+
+### ServiceStatus
+Represents the detailed status information for a service.
+
+```v
+struct ServiceStatus {
+pub mut:
+ name string // Service name
+ pid int // Process ID of the running service (if running)
+ state string // Current state of the service (Running, Success, Error, etc.)
+ target string // Target state of the service (Up, Down)
+ after map[string]string // Dependencies of the service and their states
+}
+```
+
+### ServiceStats
+Represents memory and CPU usage statistics for a service.
+
+```v
+struct ServiceStats {
+pub mut:
+ name string // Service name
+ pid int // Process ID of the service
+ memory_usage i64 // Memory usage in bytes
+ cpu_usage f64 // CPU usage as a percentage (0-100)
+ children []ChildStats // Stats for child processes
+}
+```
+
+### ChildStats
+Represents statistics for a child process.
+
+```v
+struct ChildStats {
+pub mut:
+ pid int // Process ID of the child process
+ memory_usage i64 // Memory usage in bytes
+ cpu_usage f64 // CPU usage as a percentage (0-100)
+}
+```
+
+## Error Handling
+
+The client provides comprehensive error handling through V's error system. All API methods that can fail return a result type (`!`).
+
+### ZinitError
+Custom error type for zinit-specific errors.
+
+```v
+struct ZinitError {
+pub mut:
+ code int // Error code
+ message string // Error message
+ data string // Additional error data
+}
+```
+
+Common error codes:
+- `-32000`: Service not found
+- `-32001`: Service already monitored
+- `-32002`: Service is up
+- `-32003`: Service is down
+- `-32004`: Invalid signal
+- `-32005`: Config error
+- `-32006`: Shutting down
+- `-32007`: Service already exists
+- `-32008`: Service file error
+
+## Examples
+
+See `example.v` for comprehensive usage examples covering all API methods.
+
+### Basic Service Management
+
+```v
+import zinit
+
+fn manage_service() ! {
+ mut client := zinit.new_default_client()
+ defer { client.disconnect() }
+
+ // Create a service
+ config := zinit.ServiceConfig{
+ exec: '/usr/bin/nginx'
+ oneshot: false
+ log: 'stdout'
+ after: ['network']
+ }
+
+ client.service_create('nginx', config)!
+ client.service_monitor('nginx')!
+ client.service_start('nginx')!
+
+ // Check status
+ status := client.service_status('nginx')!
+ println('Nginx is ${status.state}')
+
+ // Get statistics
+ stats := client.service_stats('nginx')!
+ println('Memory: ${stats.memory_usage / 1024 / 1024} MB')
+}
+```
+
+### Log Monitoring
+
+```v
+import zinit
+import time
+
+fn monitor_logs() ! {
+ mut client := zinit.new_default_client()
+ defer { client.disconnect() }
+
+ // Get current logs
+ logs := client.stream_current_logs(none)!
+ for log in logs {
+ println(log)
+ }
+
+ // Subscribe to new logs (simplified example)
+ for i in 0..10 {
+ log_entry := client.stream_subscribe_logs(none) or { continue }
+ println('New log: ${log_entry}')
+ time.sleep(1 * time.second)
+ }
+}
+```
+
+## Thread Safety
+
+The client is not thread-safe. If you need to use it from multiple threads, you should create separate client instances or implement your own synchronization.
+
+## Requirements
+
+- V compiler
+- Unix-like operating system (for Unix socket support)
+- Running Zinit service manager
+
+## License
+
+This module follows the same license as the parent project.
diff --git a/src/clients/zinit/client.v b/src/clients/zinit/client.v
new file mode 100644
index 0000000..7ca54e3
--- /dev/null
+++ b/src/clients/zinit/client.v
@@ -0,0 +1,211 @@
+module zinit
+
+import net.unix
+import json
+import time
+
+// service_status shows detailed status information for a specific service
+// name: the name of the service
+pub fn (mut c Client) service_status(name string) !ServiceStatus {
+ params := [name]
+ response := c.send_request('service_status', params)!
+
+ result_map := response.result as map[string]interface{}
+
+ mut after_map := map[string]string{}
+ if after_raw := result_map['after'] {
+ if after_obj := after_raw as map[string]interface{} {
+ for key, value in after_obj {
+ after_map[key] = value.str()
+ }
+ }
+ }
+
+ return ServiceStatus{
+ name: result_map['name'] or { '' }.str()
+ pid: result_map['pid'] or { 0 }.int()
+ state: result_map['state'] or { '' }.str()
+ target: result_map['target'] or { '' }.str()
+ after: after_map
+ }
+}
+
+// service_start starts a service
+// name: the name of the service to start
+pub fn (mut c Client) service_start(name string) ! {
+ params := [name]
+ c.send_request('service_start', params)!
+}
+
+// service_stop stops a service
+// name: the name of the service to stop
+pub fn (mut c Client) service_stop(name string) ! {
+ params := [name]
+ c.send_request('service_stop', params)!
+}
+
+// service_monitor starts monitoring a service
+// The service configuration is loaded from the config directory
+// name: the name of the service to monitor
+pub fn (mut c Client) service_monitor(name string) ! {
+ params := [name]
+ c.send_request('service_monitor', params)!
+}
+
+// service_forget stops monitoring a service
+// You can only forget a stopped service
+// name: the name of the service to forget
+pub fn (mut c Client) service_forget(name string) ! {
+ params := [name]
+ c.send_request('service_forget', params)!
+}
+
+// service_kill sends a signal to a running service
+// name: the name of the service to send the signal to
+// signal: the signal to send (e.g., SIGTERM, SIGKILL)
+pub fn (mut c Client) service_kill(name string, signal string) ! {
+ params := [name, signal]
+ c.send_request('service_kill', params)!
+}
+
+// service_create creates a new service configuration file
+// name: the name of the service to create
+// config: the service configuration
+pub fn (mut c Client) service_create(name string, config ServiceConfig) !string {
+ params := [name, config]
+ response := c.send_request('service_create', params)!
+ return response.result.str()
+}
+
+// service_delete deletes a service configuration file
+// name: the name of the service to delete
+pub fn (mut c Client) service_delete(name string) !string {
+ params := [name]
+ response := c.send_request('service_delete', params)!
+ return response.result.str()
+}
+
+// service_get gets a service configuration file
+// name: the name of the service to get
+pub fn (mut c Client) service_get(name string) !ServiceConfig {
+ params := [name]
+ response := c.send_request('service_get', params)!
+
+ result_map := response.result as map[string]interface{}
+
+ mut after_list := []string{}
+ if after_raw := result_map['after'] {
+ if after_array := after_raw as []interface{} {
+ for item in after_array {
+ after_list << item.str()
+ }
+ }
+ }
+
+ mut env_map := map[string]string{}
+ if env_raw := result_map['env'] {
+ if env_obj := env_raw as map[string]interface{} {
+ for key, value in env_obj {
+ env_map[key] = value.str()
+ }
+ }
+ }
+
+ return ServiceConfig{
+ exec: result_map['exec'] or { '' }.str()
+ oneshot: result_map['oneshot'] or { false }.bool()
+ after: after_list
+ log: result_map['log'] or { '' }.str()
+ env: env_map
+ shutdown_timeout: result_map['shutdown_timeout'] or { 0 }.int()
+ }
+}
+
+// service_stats gets memory and CPU usage statistics for a service
+// name: the name of the service to get stats for
+pub fn (mut c Client) service_stats(name string) !ServiceStats {
+ params := [name]
+ response := c.send_request('service_stats', params)!
+
+ result_map := response.result as map[string]interface{}
+
+ mut children_list := []ChildStats{}
+ if children_raw := result_map['children'] {
+ if children_array := children_raw as []interface{} {
+ for child_raw in children_array {
+ if child_map := child_raw as map[string]interface{} {
+ children_list << ChildStats{
+ pid: child_map['pid'] or { 0 }.int()
+ memory_usage: child_map['memory_usage'] or { i64(0) }.i64()
+ cpu_usage: child_map['cpu_usage'] or { 0.0 }.f64()
+ }
+ }
+ }
+ }
+ }
+
+ return ServiceStats{
+ name: result_map['name'] or { '' }.str()
+ pid: result_map['pid'] or { 0 }.int()
+ memory_usage: result_map['memory_usage'] or { i64(0) }.i64()
+ cpu_usage: result_map['cpu_usage'] or { 0.0 }.f64()
+ children: children_list
+ }
+}
+
+// system_shutdown stops all services and powers off the system
+pub fn (mut c Client) system_shutdown() ! {
+ c.send_request('system_shutdown', []interface{})!
+}
+
+// system_reboot stops all services and reboots the system
+pub fn (mut c Client) system_reboot() ! {
+ c.send_request('system_reboot', []interface{})!
+}
+
+// system_start_http_server starts an HTTP/RPC server at the specified address
+// address: the network address to bind the server to (e.g., '127.0.0.1:8080')
+pub fn (mut c Client) system_start_http_server(address string) !string {
+ params := [address]
+ response := c.send_request('system_start_http_server', params)!
+ return response.result.str()
+}
+
+// system_stop_http_server stops the HTTP/RPC server if running
+pub fn (mut c Client) system_stop_http_server() ! {
+ c.send_request('system_stop_http_server', []interface{})!
+}
+
+// stream_current_logs gets current logs from zinit and monitored services
+// name: optional service name filter. If provided, only logs from this service will be returned
+pub fn (mut c Client) stream_current_logs(name ?string) ![]string {
+ mut params := []interface{}
+ if service_name := name {
+ params << service_name
+ }
+
+ response := c.send_request('stream_currentLogs', params)!
+
+ if logs_array := response.result as []interface{} {
+ mut logs := []string{}
+ for log_entry in logs_array {
+ logs << log_entry.str()
+ }
+ return logs
+ }
+
+ return []string{}
+}
+
+// stream_subscribe_logs subscribes to log messages generated by zinit and monitored services
+// name: optional service name filter. If provided, only logs from this service will be returned
+// Note: This method returns a single log message. For continuous streaming, call this method repeatedly
+pub fn (mut c Client) stream_subscribe_logs(name ?string) !string {
+ mut params := []interface{}
+ if service_name := name {
+ params << service_name
+ }
+
+ response := c.send_request('stream_subscribeLogs', params)!
+ return response.result.str()
+}
diff --git a/src/clients/zinit/openrpc.json b/src/clients/zinit/openrpc.json
new file mode 100644
index 0000000..65cc7af
--- /dev/null
+++ b/src/clients/zinit/openrpc.json
@@ -0,0 +1,873 @@
+{
+ "openrpc": "1.2.6",
+ "info": {
+ "version": "1.0.0",
+ "title": "Zinit JSON-RPC API",
+ "description": "JSON-RPC 2.0 API for controlling and querying Zinit services",
+ "license": {
+ "name": "MIT"
+ }
+ },
+ "servers": [
+ {
+ "name": "Unix Socket",
+ "url": "unix:///tmp/zinit.sock"
+ }
+ ],
+ "methods": [
+ {
+ "name": "rpc_discover",
+ "description": "Returns the OpenRPC specification for the API",
+ "params": [],
+ "result": {
+ "name": "OpenRPCSpec",
+ "description": "The OpenRPC specification",
+ "schema": {
+ "type": "object"
+ }
+ },
+ "examples": [
+ {
+ "name": "Get API specification",
+ "params": [],
+ "result": {
+ "name": "OpenRPCSpecResult",
+ "value": {
+ "openrpc": "1.2.6",
+ "info": {
+ "version": "1.0.0",
+ "title": "Zinit JSON-RPC API"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "service_list",
+ "description": "Lists all services managed by Zinit",
+ "params": [],
+ "result": {
+ "name": "ServiceList",
+ "description": "A map of service names to their current states",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string",
+ "description": "Service state (Running, Success, Error, etc.)"
+ }
+ }
+ },
+ "examples": [
+ {
+ "name": "List all services",
+ "params": [],
+ "result": {
+ "name": "ServiceListResult",
+ "value": {
+ "service1": "Running",
+ "service2": "Success",
+ "service3": "Error"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "service_status",
+ "description": "Shows detailed status information for a specific service",
+ "params": [
+ {
+ "name": "name",
+ "description": "The name of the service",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "result": {
+ "name": "ServiceStatus",
+ "description": "Detailed status information for the service",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Service name"
+ },
+ "pid": {
+ "type": "integer",
+ "description": "Process ID of the running service (if running)"
+ },
+ "state": {
+ "type": "string",
+ "description": "Current state of the service (Running, Success, Error, etc.)"
+ },
+ "target": {
+ "type": "string",
+ "description": "Target state of the service (Up, Down)"
+ },
+ "after": {
+ "type": "object",
+ "description": "Dependencies of the service and their states",
+ "additionalProperties": {
+ "type": "string",
+ "description": "State of the dependency"
+ }
+ }
+ }
+ }
+ },
+ "examples": [
+ {
+ "name": "Get status of redis service",
+ "params": [
+ {
+ "name": "name",
+ "value": "redis"
+ }
+ ],
+ "result": {
+ "name": "ServiceStatusResult",
+ "value": {
+ "name": "redis",
+ "pid": 1234,
+ "state": "Running",
+ "target": "Up",
+ "after": {
+ "dependency1": "Success",
+ "dependency2": "Running"
+ }
+ }
+ }
+ }
+ ],
+ "errors": [
+ {
+ "code": -32000,
+ "message": "Service not found",
+ "data": "service name \"unknown\" unknown"
+ }
+ ]
+ },
+ {
+ "name": "service_start",
+ "description": "Starts a service",
+ "params": [
+ {
+ "name": "name",
+ "description": "The name of the service to start",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "result": {
+ "name": "StartResult",
+ "description": "Result of the start operation",
+ "schema": {
+ "type": "null"
+ }
+ },
+ "examples": [
+ {
+ "name": "Start redis service",
+ "params": [
+ {
+ "name": "name",
+ "value": "redis"
+ }
+ ],
+ "result": {
+ "name": "StartResult",
+ "value": null
+ }
+ }
+ ],
+ "errors": [
+ {
+ "code": -32000,
+ "message": "Service not found",
+ "data": "service name \"unknown\" unknown"
+ }
+ ]
+ },
+ {
+ "name": "service_stop",
+ "description": "Stops a service",
+ "params": [
+ {
+ "name": "name",
+ "description": "The name of the service to stop",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "result": {
+ "name": "StopResult",
+ "description": "Result of the stop operation",
+ "schema": {
+ "type": "null"
+ }
+ },
+ "examples": [
+ {
+ "name": "Stop redis service",
+ "params": [
+ {
+ "name": "name",
+ "value": "redis"
+ }
+ ],
+ "result": {
+ "name": "StopResult",
+ "value": null
+ }
+ }
+ ],
+ "errors": [
+ {
+ "code": -32000,
+ "message": "Service not found",
+ "data": "service name \"unknown\" unknown"
+ },
+ {
+ "code": -32003,
+ "message": "Service is down",
+ "data": "service \"redis\" is down"
+ }
+ ]
+ },
+ {
+ "name": "service_monitor",
+ "description": "Starts monitoring a service. The service configuration is loaded from the config directory.",
+ "params": [
+ {
+ "name": "name",
+ "description": "The name of the service to monitor",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "result": {
+ "name": "MonitorResult",
+ "description": "Result of the monitor operation",
+ "schema": {
+ "type": "null"
+ }
+ },
+ "examples": [
+ {
+ "name": "Monitor redis service",
+ "params": [
+ {
+ "name": "name",
+ "value": "redis"
+ }
+ ],
+ "result": {
+ "name": "MonitorResult",
+ "value": null
+ }
+ }
+ ],
+ "errors": [
+ {
+ "code": -32001,
+ "message": "Service already monitored",
+ "data": "service \"redis\" already monitored"
+ },
+ {
+ "code": -32005,
+ "message": "Config error",
+ "data": "failed to load service configuration"
+ }
+ ]
+ },
+ {
+ "name": "service_forget",
+ "description": "Stops monitoring a service. You can only forget a stopped service.",
+ "params": [
+ {
+ "name": "name",
+ "description": "The name of the service to forget",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "result": {
+ "name": "ForgetResult",
+ "description": "Result of the forget operation",
+ "schema": {
+ "type": "null"
+ }
+ },
+ "examples": [
+ {
+ "name": "Forget redis service",
+ "params": [
+ {
+ "name": "name",
+ "value": "redis"
+ }
+ ],
+ "result": {
+ "name": "ForgetResult",
+ "value": null
+ }
+ }
+ ],
+ "errors": [
+ {
+ "code": -32000,
+ "message": "Service not found",
+ "data": "service name \"unknown\" unknown"
+ },
+ {
+ "code": -32002,
+ "message": "Service is up",
+ "data": "service \"redis\" is up"
+ }
+ ]
+ },
+ {
+ "name": "service_kill",
+ "description": "Sends a signal to a running service",
+ "params": [
+ {
+ "name": "name",
+ "description": "The name of the service to send the signal to",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "signal",
+ "description": "The signal to send (e.g., SIGTERM, SIGKILL)",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "result": {
+ "name": "KillResult",
+ "description": "Result of the kill operation",
+ "schema": {
+ "type": "null"
+ }
+ },
+ "examples": [
+ {
+ "name": "Send SIGTERM to redis service",
+ "params": [
+ {
+ "name": "name",
+ "value": "redis"
+ },
+ {
+ "name": "signal",
+ "value": "SIGTERM"
+ }
+ ],
+ "result": {
+ "name": "KillResult",
+ "value": null
+ }
+ }
+ ],
+ "errors": [
+ {
+ "code": -32000,
+ "message": "Service not found",
+ "data": "service name \"unknown\" unknown"
+ },
+ {
+ "code": -32003,
+ "message": "Service is down",
+ "data": "service \"redis\" is down"
+ },
+ {
+ "code": -32004,
+ "message": "Invalid signal",
+ "data": "invalid signal: INVALID"
+ }
+ ]
+ },
+ {
+ "name": "system_shutdown",
+ "description": "Stops all services and powers off the system",
+ "params": [],
+ "result": {
+ "name": "ShutdownResult",
+ "description": "Result of the shutdown operation",
+ "schema": {
+ "type": "null"
+ }
+ },
+ "examples": [
+ {
+ "name": "Shutdown the system",
+ "params": [],
+ "result": {
+ "name": "ShutdownResult",
+ "value": null
+ }
+ }
+ ],
+ "errors": [
+ {
+ "code": -32006,
+ "message": "Shutting down",
+ "data": "system is already shutting down"
+ }
+ ]
+ },
+ {
+ "name": "system_reboot",
+ "description": "Stops all services and reboots the system",
+ "params": [],
+ "result": {
+ "name": "RebootResult",
+ "description": "Result of the reboot operation",
+ "schema": {
+ "type": "null"
+ }
+ },
+ "examples": [
+ {
+ "name": "Reboot the system",
+ "params": [],
+ "result": {
+ "name": "RebootResult",
+ "value": null
+ }
+ }
+ ],
+ "errors": [
+ {
+ "code": -32006,
+ "message": "Shutting down",
+ "data": "system is already shutting down"
+ }
+ ]
+ },
+ {
+ "name": "service_create",
+ "description": "Creates a new service configuration file",
+ "params": [
+ {
+ "name": "name",
+ "description": "The name of the service to create",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "content",
+ "description": "The service configuration content",
+ "required": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "exec": {
+ "type": "string",
+ "description": "Command to run"
+ },
+ "oneshot": {
+ "type": "boolean",
+ "description": "Whether the service should be restarted"
+ },
+ "after": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Services that must be running before this one starts"
+ },
+ "log": {
+ "type": "string",
+ "enum": ["null", "ring", "stdout"],
+ "description": "How to handle service output"
+ },
+ "env": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "description": "Environment variables for the service"
+ },
+ "shutdown_timeout": {
+ "type": "integer",
+ "description": "Maximum time to wait for service to stop during shutdown"
+ }
+ }
+ }
+ }
+ ],
+ "result": {
+ "name": "CreateServiceResult",
+ "description": "Result of the create operation",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "errors": [
+ {
+ "code": -32007,
+ "message": "Service already exists",
+ "data": "Service 'name' already exists"
+ },
+ {
+ "code": -32008,
+ "message": "Service file error",
+ "data": "Failed to create service file"
+ }
+ ]
+ },
+ {
+ "name": "service_delete",
+ "description": "Deletes a service configuration file",
+ "params": [
+ {
+ "name": "name",
+ "description": "The name of the service to delete",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "result": {
+ "name": "DeleteServiceResult",
+ "description": "Result of the delete operation",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "errors": [
+ {
+ "code": -32000,
+ "message": "Service not found",
+ "data": "Service 'name' not found"
+ },
+ {
+ "code": -32008,
+ "message": "Service file error",
+ "data": "Failed to delete service file"
+ }
+ ]
+ },
+ {
+ "name": "service_get",
+ "description": "Gets a service configuration file",
+ "params": [
+ {
+ "name": "name",
+ "description": "The name of the service to get",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "result": {
+ "name": "GetServiceResult",
+ "description": "The service configuration",
+ "schema": {
+ "type": "object"
+ }
+ },
+ "errors": [
+ {
+ "code": -32000,
+ "message": "Service not found",
+ "data": "Service 'name' not found"
+ },
+ {
+ "code": -32008,
+ "message": "Service file error",
+ "data": "Failed to read service file"
+ }
+ ]
+ },
+ {
+ "name": "service_stats",
+ "description": "Get memory and CPU usage statistics for a service",
+ "params": [
+ {
+ "name": "name",
+ "description": "The name of the service to get stats for",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "result": {
+ "name": "ServiceStats",
+ "description": "Memory and CPU usage statistics for the service",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Service name"
+ },
+ "pid": {
+ "type": "integer",
+ "description": "Process ID of the service"
+ },
+ "memory_usage": {
+ "type": "integer",
+ "description": "Memory usage in bytes"
+ },
+ "cpu_usage": {
+ "type": "number",
+ "description": "CPU usage as a percentage (0-100)"
+ },
+ "children": {
+ "type": "array",
+ "description": "Stats for child processes",
+ "items": {
+ "type": "object",
+ "properties": {
+ "pid": {
+ "type": "integer",
+ "description": "Process ID of the child process"
+ },
+ "memory_usage": {
+ "type": "integer",
+ "description": "Memory usage in bytes"
+ },
+ "cpu_usage": {
+ "type": "number",
+ "description": "CPU usage as a percentage (0-100)"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "examples": [
+ {
+ "name": "Get stats for redis service",
+ "params": [
+ {
+ "name": "name",
+ "value": "redis"
+ }
+ ],
+ "result": {
+ "name": "ServiceStatsResult",
+ "value": {
+ "name": "redis",
+ "pid": 1234,
+ "memory_usage": 10485760,
+ "cpu_usage": 2.5,
+ "children": [
+ {
+ "pid": 1235,
+ "memory_usage": 5242880,
+ "cpu_usage": 1.2
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "errors": [
+ {
+ "code": -32000,
+ "message": "Service not found",
+ "data": "service name \"unknown\" unknown"
+ },
+ {
+ "code": -32003,
+ "message": "Service is down",
+ "data": "service \"redis\" is down"
+ }
+ ]
+ },
+ {
+ "name": "system_start_http_server",
+ "description": "Start an HTTP/RPC server at the specified address",
+ "params": [
+ {
+ "name": "address",
+ "description": "The network address to bind the server to (e.g., '127.0.0.1:8080')",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "result": {
+ "name": "StartHttpServerResult",
+ "description": "Result of the start HTTP server operation",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "examples": [
+ {
+ "name": "Start HTTP server on localhost:8080",
+ "params": [
+ {
+ "name": "address",
+ "value": "127.0.0.1:8080"
+ }
+ ],
+ "result": {
+ "name": "StartHttpServerResult",
+ "value": "HTTP server started at 127.0.0.1:8080"
+ }
+ }
+ ],
+ "errors": [
+ {
+ "code": -32602,
+ "message": "Invalid address",
+ "data": "Invalid network address format"
+ }
+ ]
+ },
+ {
+ "name": "system_stop_http_server",
+ "description": "Stop the HTTP/RPC server if running",
+ "params": [],
+ "result": {
+ "name": "StopHttpServerResult",
+ "description": "Result of the stop HTTP server operation",
+ "schema": {
+ "type": "null"
+ }
+ },
+ "examples": [
+ {
+ "name": "Stop the HTTP server",
+ "params": [],
+ "result": {
+ "name": "StopHttpServerResult",
+ "value": null
+ }
+ }
+ ],
+ "errors": [
+ {
+ "code": -32602,
+ "message": "Server not running",
+ "data": "No HTTP server is currently running"
+ }
+ ]
+ },
+ {
+ "name": "stream_currentLogs",
+ "description": "Get current logs from zinit and monitored services",
+ "params": [
+ {
+ "name": "name",
+ "description": "Optional service name filter. If provided, only logs from this service will be returned",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "result": {
+ "name": "LogsResult",
+ "description": "Array of log strings",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "examples": [
+ {
+ "name": "Get all logs",
+ "params": [],
+ "result": {
+ "name": "LogsResult",
+ "value": [
+ "2023-01-01T12:00:00 redis: Starting service",
+ "2023-01-01T12:00:01 nginx: Starting service"
+ ]
+ }
+ },
+ {
+ "name": "Get logs for a specific service",
+ "params": [
+ {
+ "name": "name",
+ "value": "redis"
+ }
+ ],
+ "result": {
+ "name": "LogsResult",
+ "value": [
+ "2023-01-01T12:00:00 redis: Starting service",
+ "2023-01-01T12:00:02 redis: Service started"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "name": "stream_subscribeLogs",
+ "description": "Subscribe to log messages generated by zinit and monitored services",
+ "params": [
+ {
+ "name": "name",
+ "description": "Optional service name filter. If provided, only logs from this service will be returned",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "result": {
+ "name": "LogSubscription",
+ "description": "A subscription to log messages",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "examples": [
+ {
+ "name": "Subscribe to all logs",
+ "params": [],
+ "result": {
+ "name": "LogSubscription",
+ "value": "2023-01-01T12:00:00 redis: Service started"
+ }
+ },
+ {
+ "name": "Subscribe to filtered logs",
+ "params": [
+ {
+ "name": "name",
+ "value": "redis"
+ }
+ ],
+ "result": {
+ "name": "LogSubscription",
+ "value": "2023-01-01T12:00:00 redis: Service started"
+ }
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/clients/zinit/openrpc.v b/src/clients/zinit/openrpc.v
new file mode 100644
index 0000000..0172eb0
--- /dev/null
+++ b/src/clients/zinit/openrpc.v
@@ -0,0 +1,161 @@
+module zinit
+
+import freeflowuniverse.herolib.schemas.jsonrpc
+import net.unix
+import time
+import json
+
+// UnixSocketTransport implements the jsonrpc.IRPCTransportClient interface for Unix domain sockets
+struct UnixSocketTransport {
+mut:
+ socket_path string
+}
+
+// new_unix_socket_transport creates a new Unix socket transport
+fn new_unix_socket_transport(socket_path string) &UnixSocketTransport {
+ return &UnixSocketTransport{
+ socket_path: socket_path
+ }
+}
+
+// send implements the jsonrpc.IRPCTransportClient interface
+fn (mut t UnixSocketTransport) send(request string, params jsonrpc.SendParams) !string {
+ // Create a Unix domain socket client
+ mut socket := unix.connect_stream(t.socket_path)!
+ defer { socket.close() or {} }
+
+ // Set timeout if specified
+ if params.timeout > 0 {
+ socket.set_read_timeout(params.timeout * time.second)
+ socket.set_write_timeout(params.timeout * time.second)
+ }
+
+ // Send the request
+ socket.write_string(request + '\n')!
+
+ // Read the response
+ mut response := ''
+ mut buf := []u8{len: 4096}
+
+ for {
+ bytes_read := socket.read(mut buf)!
+ if bytes_read <= 0 {
+ break
+ }
+ response += buf[..bytes_read].bytestr()
+
+ // Check if we've received a complete JSON response
+ if response.ends_with('}') {
+ break
+ }
+ }
+
+ return response
+}
+
+// Client provides a client interface to the zinit JSON-RPC API over Unix socket
+@[heap]
+pub struct Client {
+mut:
+ socket_path string
+ rpc_client &jsonrpc.Client
+ request_id int
+}
+
+// new_client creates a new zinit client instance
+// socket_path: path to the Unix socket (default: /tmp/zinit.sock)
+pub fn new_client(socket_path string) &Client {
+ mut transport := new_unix_socket_transport(socket_path)
+ mut rpc_client := jsonrpc.new_client(transport)
+
+ return &Client{
+ socket_path: socket_path
+ rpc_client: rpc_client
+ request_id: 0
+ }
+}
+
+// new_default_client creates a new zinit client with default socket path
+pub fn new_default_client() &Client {
+ return new_client('/tmp/zinit.sock')
+}
+
+// rpc_discover returns the OpenRPC specification for the API
+pub fn (mut c Client) rpc_discover() !map[string]json.Any {
+ send_params := jsonrpc.SendParams{
+ timeout: 30
+ retry: 1
+ }
+
+ request := jsonrpc.new_request_generic('rpc_discover', []string{})
+ result := c.rpc_client.send[[]string, map[string]json.Any](request, send_params)!
+
+ return result
+}
+
+// service_list lists all services managed by Zinit
+// Returns a map of service names to their current states
+pub fn (mut c Client) service_list() !map[string]string {
+ send_params := jsonrpc.SendParams{
+ timeout: 30
+ retry: 1
+ }
+
+ request := jsonrpc.new_request_generic('service_list', []string{})
+ result := c.rpc_client.send[[]string, map[string]string](request, send_params)!
+
+ return result
+}
+
+// service_status shows detailed status information for a specific service
+// name: the name of the service
+pub fn (mut c Client) service_status(name string) !ServiceStatus {
+ send_params := jsonrpc.SendParams{
+ timeout: 30
+ retry: 1
+ }
+
+ request := jsonrpc.new_request_generic('service_status', [name])
+ result_map := c.rpc_client.send[[string], map[string]json.Any](request, send_params)!
+
+ mut after_map := map[string]string{}
+ if after_raw := result_map['after'] {
+ if after_raw is map[string]json.Any {
+ for key, value in after_raw {
+ after_map[key] = value.str()
+ }
+ }
+ }
+
+ return ServiceStatus{
+ name: result_map['name'].str()
+ pid: result_map['pid'].int()
+ state: result_map['state'].str()
+ target: result_map['target'].str()
+ after: after_map
+ }
+}
+
+// service_start starts a service
+// name: the name of the service to start
+pub fn (mut c Client) service_start(name string) ! {
+ send_params := jsonrpc.SendParams{
+ timeout: 30
+ retry: 1
+ }
+
+ request := jsonrpc.new_request_generic('service_start', [name])
+ c.rpc_client.send[[string], json.Any](request, send_params)!
+}
+
+// service_stop stops a service
+// name: the name of the service to stop
+pub fn (mut c Client) service_stop(name string) ! {
+ send_params := jsonrpc.SendParams{
+ timeout: 30
+ retry: 1
+ }
+
+ request := jsonrpc.new_request_generic('service_stop', [name])
+ c.rpc_client.send[[string], json.Any](request, send_params)!
+}
diff --git a/src/clients/zinit/test.v b/src/clients/zinit/test.v
new file mode 100644
index 0000000..299e9d3
--- /dev/null
+++ b/src/clients/zinit/test.v
@@ -0,0 +1,204 @@
+module zinit
+
+import time
+
+// Basic tests for the zinit client module
+// Note: These tests require a running zinit instance at /tmp/zinit.sock
+
+// test_client_creation tests basic client creation
+fn test_client_creation() {
+ // Test default client creation
+ client1 := new_default_client()
+ assert client1.socket_path == '/tmp/zinit.sock'
+
+ // Test custom client creation
+ client2 := new_client('/custom/path/zinit.sock')
+ assert client2.socket_path == '/custom/path/zinit.sock'
+
+ println('✓ Client creation tests passed')
+}
+
+// test_service_config tests ServiceConfig struct
+fn test_service_config() {
+ config := ServiceConfig{
+ exec: '/usr/bin/test'
+ oneshot: true
+ after: ['dependency1', 'dependency2']
+ log: 'stdout'
+ env: {
+ 'TEST_VAR': 'test_value'
+ 'PATH': '/usr/bin:/bin'
+ }
+ shutdown_timeout: 30
+ }
+
+ assert config.exec == '/usr/bin/test'
+ assert config.oneshot == true
+ assert config.after.len == 2
+ assert config.after[0] == 'dependency1'
+ assert config.env['TEST_VAR'] == 'test_value'
+ assert config.shutdown_timeout == 30
+
+ println('✓ ServiceConfig tests passed')
+}
+
+// test_service_status tests ServiceStatus struct
+fn test_service_status() {
+ mut after_map := map[string]string{}
+ after_map['dep1'] = 'Running'
+ after_map['dep2'] = 'Success'
+
+ status := ServiceStatus{
+ name: 'test_service'
+ pid: 1234
+ state: 'Running'
+ target: 'Up'
+ after: after_map
+ }
+
+ assert status.name == 'test_service'
+ assert status.pid == 1234
+ assert status.state == 'Running'
+ assert status.target == 'Up'
+ assert status.after['dep1'] == 'Running'
+
+ println('✓ ServiceStatus tests passed')
+}
+
+// test_service_stats tests ServiceStats and ChildStats structs
+fn test_service_stats() {
+ child1 := ChildStats{
+ pid: 1235
+ memory_usage: 1024 * 1024 // 1MB
+ cpu_usage: 2.5
+ }
+
+ child2 := ChildStats{
+ pid: 1236
+ memory_usage: 2 * 1024 * 1024 // 2MB
+ cpu_usage: 1.2
+ }
+
+ stats := ServiceStats{
+ name: 'test_service'
+ pid: 1234
+ memory_usage: 10 * 1024 * 1024 // 10MB
+ cpu_usage: 5.0
+ children: [child1, child2]
+ }
+
+ assert stats.name == 'test_service'
+ assert stats.pid == 1234
+ assert stats.memory_usage == 10 * 1024 * 1024
+ assert stats.cpu_usage == 5.0
+ assert stats.children.len == 2
+ assert stats.children[0].pid == 1235
+ assert stats.children[1].memory_usage == 2 * 1024 * 1024
+
+ println('✓ ServiceStats tests passed')
+}
+
+// test_zinit_error tests ZinitError struct and error message
+fn test_zinit_error() {
+ error := ZinitError{
+ code: -32000
+ message: 'Service not found'
+ data: 'service name "unknown" unknown'
+ }
+
+ assert error.code == -32000
+ assert error.message == 'Service not found'
+ assert error.data == 'service name "unknown" unknown'
+
+ error_msg := error.msg()
+ assert error_msg.contains('Zinit Error -32000')
+ assert error_msg.contains('Service not found')
+ assert error_msg.contains('service name "unknown" unknown')
+
+ println('✓ ZinitError tests passed')
+}
+
+// test_connection_handling tests connection management (without actual connection)
+fn test_connection_handling() {
+ mut client := new_default_client()
+
+ // Initially no connection
+ assert client.conn == none
+
+ // Test disconnect on non-connected client (should not panic)
+ client.disconnect()
+ assert client.conn == none
+
+ println('✓ Connection handling tests passed')
+}
+
+// integration_test_basic performs basic integration tests if zinit is available
+fn integration_test_basic() {
+ mut client := new_default_client()
+ defer {
+ client.disconnect()
+ }
+
+ println('Running integration tests (requires running zinit)...')
+
+ // Test RPC discovery
+ spec := client.rpc_discover() or {
+ println('⚠ Integration test skipped: zinit not available (${err})')
+ return
+ }
+
+ println('✓ RPC discovery successful')
+
+ // Test service list
+ services := client.service_list() or {
+ println('✗ Service list failed: ${err}')
+ return
+ }
+
+ println('✓ Service list successful (${services.len} services)')
+
+ // If there are services, test getting status of the first one
+ if services.len > 0 {
+ service_name := services.keys()[0]
+ status := client.service_status(service_name) or {
+ println('⚠ Could not get status for ${service_name}: ${err}')
+ return
+ }
+ println('✓ Service status for ${service_name}: ${status.state}')
+ }
+
+ // Test getting current logs
+ logs := client.stream_current_logs(none) or {
+ println('⚠ Could not get logs: ${err}')
+ return
+ }
+
+ println('✓ Log streaming successful (${logs.len} log entries)')
+
+ println('✓ All integration tests passed')
+}
+
+// run_all_tests runs all test functions
+pub fn run_all_tests() {
+ println('Running Zinit Client Tests...\n')
+
+ // Unit tests
+ test_client_creation()
+ test_service_config()
+ test_service_status()
+ test_service_stats()
+ test_zinit_error()
+ test_connection_handling()
+
+ println('\n--- Unit Tests Complete ---\n')
+
+ // Integration tests (optional, requires running zinit)
+ integration_test_basic()
+
+ println('\n--- All Tests Complete ---')
+}
+
+// main function for running tests directly
+fn main() {
+ run_all_tests()
+}
diff --git a/src/clients/zinit/types.v b/src/clients/zinit/types.v
new file mode 100644
index 0000000..62adf03
--- /dev/null
+++ b/src/clients/zinit/types.v
@@ -0,0 +1,53 @@
+module zinit
+
+// ServiceConfig represents the configuration for a zinit service
+pub struct ServiceConfig {
+pub mut:
+ exec string // Command to run
+ oneshot bool // Whether the service should be restarted
+ after []string // Services that must be running before this one starts
+ log string // How to handle service output (null, ring, stdout)
+ env map[string]string // Environment variables for the service
+ shutdown_timeout int // Maximum time to wait for service to stop during shutdown
+}
+
+// ServiceStatus represents the detailed status information for a service
+pub struct ServiceStatus {
+pub mut:
+ name string // Service name
+ pid int // Process ID of the running service (if running)
+ state string // Current state of the service (Running, Success, Error, etc.)
+ target string // Target state of the service (Up, Down)
+ after map[string]string // Dependencies of the service and their states
+}
+
+// ServiceStats represents memory and CPU usage statistics for a service
+pub struct ServiceStats {
+pub mut:
+ name string // Service name
+ pid int // Process ID of the service
+ memory_usage i64 // Memory usage in bytes
+ cpu_usage f64 // CPU usage as a percentage (0-100)
+ children []ChildStats // Stats for child processes
+}
+
+// ChildStats represents statistics for a child process
+pub struct ChildStats {
+pub mut:
+ pid int // Process ID of the child process
+ memory_usage i64 // Memory usage in bytes
+ cpu_usage f64 // CPU usage as a percentage (0-100)
+}
+
+// ZinitError represents an error returned by the zinit API
+pub struct ZinitError {
+pub mut:
+ code int // Error code
+ message string // Error message
+ data string // Additional error data
+}
+
+// Error implements the error interface for ZinitError
+pub fn (e ZinitError) msg() string {
+ return 'Zinit Error ${e.code}: ${e.message} - ${e.data}'
+}
diff --git a/src/clients/zinit/zinit.v b/src/clients/zinit/zinit.v
new file mode 100644
index 0000000..26c7957
--- /dev/null
+++ b/src/clients/zinit/zinit.v
@@ -0,0 +1,145 @@
+module zinit
+
+// Zinit Client Module
+//
+// This module provides a comprehensive V (Vlang) client library for interacting
+// with the Zinit service manager via JSON-RPC over Unix socket.
+//
+// The module includes:
+// - Complete type definitions for all API structures
+// - Full client implementation with all OpenRPC methods
+// - Comprehensive error handling
+// - Connection management
+// - Well-documented API with examples
+//
+// Usage:
+// import zinit
+//
+// mut client := zinit.new_default_client()
+// defer { client.disconnect() }
+//
+// services := client.service_list()!
+// for name, state in services {
+// println('${name}: ${state}')
+// }
+//
+// For detailed documentation, see README.md
+// For usage examples, see example.v
+// For tests, see test.v
+
+// Module version information
+pub const (
+ version = '1.0.0'
+ author = 'Hero Code'
+ license = 'MIT'
+)
+
+// Default socket path for zinit
+pub const default_socket_path = '/tmp/zinit.sock'
+
+// Common service states
+pub const (
+ state_running = 'Running'
+ state_success = 'Success'
+ state_error = 'Error'
+ state_stopped = 'Stopped'
+ state_failed = 'Failed'
+)
+
+// Common service targets
+pub const (
+ target_up = 'Up'
+ target_down = 'Down'
+)
+
+// Common log types
+pub const (
+ log_null = 'null'
+ log_ring = 'ring'
+ log_stdout = 'stdout'
+)
+
+// Common signals
+pub const (
+ signal_term = 'SIGTERM'
+ signal_kill = 'SIGKILL'
+ signal_hup = 'SIGHUP'
+ signal_usr1 = 'SIGUSR1'
+ signal_usr2 = 'SIGUSR2'
+)
+
+// JSON-RPC error codes as defined in the OpenRPC specification
+pub const (
+ error_service_not_found = -32000
+ error_service_already_monitored = -32001
+ error_service_is_up = -32002
+ error_service_is_down = -32003
+ error_invalid_signal = -32004
+ error_config_error = -32005
+ error_shutting_down = -32006
+ error_service_already_exists = -32007
+ error_service_file_error = -32008
+)
+
+// Helper function to create a basic service configuration
+pub fn new_service_config(exec string) ServiceConfig {
+ return ServiceConfig{
+ exec: exec
+ oneshot: false
+ log: log_stdout
+ env: map[string]string{}
+ shutdown_timeout: 30
+ }
+}
+
+// Helper function to create a oneshot service configuration
+pub fn new_oneshot_service_config(exec string) ServiceConfig {
+ return ServiceConfig{
+ exec: exec
+ oneshot: true
+ log: log_stdout
+ env: map[string]string{}
+ shutdown_timeout: 30
+ }
+}
+
+// Helper function to check if an error is a specific zinit error code
+pub fn is_zinit_error_code(err IError, code int) bool {
+ if zinit_err := err as ZinitError {
+ return zinit_err.code == code
+ }
+ return false
+}
+
+// Helper function to check if service is not found error
+pub fn is_service_not_found_error(err IError) bool {
+ return is_zinit_error_code(err, error_service_not_found)
+}
+
+// Helper function to check if service is already monitored error
+pub fn is_service_already_monitored_error(err IError) bool {
+ return is_zinit_error_code(err, error_service_already_monitored)
+}
+
+// Helper function to check if service is down error
+pub fn is_service_down_error(err IError) bool {
+ return is_zinit_error_code(err, error_service_is_down)
+}
+
+// Helper function to format memory usage in human-readable format
+pub fn format_memory_usage(bytes i64) string {
+ if bytes < 1024 {
+ return '${bytes} B'
+ } else if bytes < 1024 * 1024 {
+ return '${bytes / 1024} KB'
+ } else if bytes < 1024 * 1024 * 1024 {
+ return '${bytes / 1024 / 1024} MB'
+ } else {
+ return '${bytes / 1024 / 1024 / 1024} GB'
+ }
+}
+
+// Helper function to format CPU usage
+pub fn format_cpu_usage(cpu_percent f64) string {
+ return '${cpu_percent:.1f}%'
+}