Skip to content

Commit df10c1f

Browse files
authored
Feature/service agents (#63)
1 parent 1094d3e commit df10c1f

13 files changed

Lines changed: 1165 additions & 128 deletions

cmd/pilotctl/main.go

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,13 +417,14 @@ Mailbox:
417417
pilotctl received [--clear]
418418
pilotctl inbox [--clear]
419419
420-
Scriptorium (low-level responder dispatch):
420+
Service Agents (AI-powered overlay services):
421+
pilotctl tui Interactive TUI for all service agents
422+
pilotctl ls List available service agents and config status
423+
421424
pilotctl scriptorium <command> [body] [--node <address|hostname>]
422425
pilotctl scriptorium polymarket "from: 2026-04-01T00:00:00Z, to: 2026-04-02T00:00:00Z"
423426
pilotctl scriptorium stockmarket "from: 2026-04-01"
424-
Config: ~/.pilot/scriptorium.yaml (node: <pilot-address>)
425427
426-
Service Agents (AI-powered overlay services):
427428
pilotctl ai "<query>" [--node <address>] [--timeout <dur>] [--output-file [path]]
428429
pilotctl clawdit ["<query>"] [--file <openclaw.json>] [--node <address>] [--timeout <dur>] [--output-file [path]]
429430
@@ -799,6 +800,10 @@ func main() {
799800
cmdAi(cmdArgs)
800801
case "clawdit":
801802
cmdClawdit(cmdArgs)
803+
case "ls":
804+
cmdLs(cmdArgs)
805+
case "tui":
806+
cmdTui(cmdArgs)
802807

803808
// Internal: forked daemon process
804809
case "_daemon-run":
@@ -1132,6 +1137,31 @@ func cmdContext() {
11321137
"description": "List messages received via data exchange (port 1001). Messages saved to ~/.pilot/inbox/. Use --clear to delete all",
11331138
"returns": "messages [{type, from, data, received_at}], total, dir",
11341139
},
1140+
"scriptorium": map[string]interface{}{
1141+
"args": []string{"<command>", "[body]", "[--node <address|hostname>]"},
1142+
"description": "Send a command to a Scriptorium service agent (e.g. stockmarket, polymarket)",
1143+
"returns": "command, body, node, ack",
1144+
},
1145+
"ai": map[string]interface{}{
1146+
"args": []string{"<query>", "[--node <address>]", "[--timeout <dur>]", "[--output-file [path]]"},
1147+
"description": "Send a natural-language query to the AI assistant and wait for the reply",
1148+
"returns": "reply",
1149+
},
1150+
"clawdit": map[string]interface{}{
1151+
"args": []string{"[query]", "[--file <openclaw.json>]", "[--node <address>]", "[--timeout <dur>]", "[--output-file [path]]"},
1152+
"description": "Run a security audit of an OpenClaw installation",
1153+
"returns": "reply",
1154+
},
1155+
"ls": map[string]interface{}{
1156+
"args": []string{},
1157+
"description": "List all available service agents and their configuration status",
1158+
"returns": "agents [{name, description, usage, config, configured, node}]",
1159+
},
1160+
"tui": map[string]interface{}{
1161+
"args": []string{},
1162+
"description": "Launch the interactive service-agent TUI (chat with AI, query Scriptorium, run audits)",
1163+
"returns": "(interactive — no JSON output)",
1164+
},
11351165
},
11361166
"error_codes": map[string]interface{}{
11371167
"invalid_argument": "Bad input or usage error (do not retry)",
@@ -5529,6 +5559,124 @@ func writeServiceAgentReply(reply, outputFile, prefix string) {
55295559
}
55305560
}
55315561

5562+
// cmdLs lists all available service agents and their configuration status.
5563+
func cmdLs(args []string) {
5564+
type agentInfo struct {
5565+
Name string `json:"name"`
5566+
Description string `json:"description"`
5567+
Usage string `json:"usage"`
5568+
Config string `json:"config"`
5569+
Configured bool `json:"configured"`
5570+
Node string `json:"node,omitempty"`
5571+
}
5572+
5573+
agents := []agentInfo{
5574+
{
5575+
Name: "ai",
5576+
Description: "Natural-language assistant — ask anything about your network",
5577+
Usage: `pilotctl ai "<query>"`,
5578+
Config: "~/.pilot/scriptorium.yaml",
5579+
},
5580+
{
5581+
Name: "scriptorium",
5582+
Description: "Intelligence briefs (stockmarket, polymarket, …)",
5583+
Usage: `pilotctl scriptorium <command> "<body>"`,
5584+
Config: "~/.pilot/scriptorium.yaml",
5585+
},
5586+
{
5587+
Name: "clawdit",
5588+
Description: "Security audit of an OpenClaw installation",
5589+
Usage: `pilotctl clawdit ["<query>"] [--file <path>]`,
5590+
Config: "~/.pilot/clawdit.yaml",
5591+
},
5592+
}
5593+
5594+
// Resolve configured state.
5595+
scriptoriumNode := loadScriptoriumNode()
5596+
clawditNode := loadClawditNode()
5597+
for i := range agents {
5598+
switch agents[i].Name {
5599+
case "ai", "scriptorium":
5600+
if scriptoriumNode != "" {
5601+
agents[i].Configured = true
5602+
agents[i].Node = scriptoriumNode
5603+
}
5604+
case "clawdit":
5605+
if clawditNode != "" {
5606+
agents[i].Configured = true
5607+
agents[i].Node = clawditNode
5608+
}
5609+
}
5610+
}
5611+
5612+
if jsonOutput {
5613+
outputOK(map[string]interface{}{"agents": agents})
5614+
return
5615+
}
5616+
5617+
fmt.Println("Service Agents:")
5618+
fmt.Println()
5619+
for _, a := range agents {
5620+
status := " ✗ not configured"
5621+
if a.Configured {
5622+
status = fmt.Sprintf(" ✓ node %s", a.Node)
5623+
}
5624+
fmt.Printf(" %-14s %s\n", a.Name, a.Description)
5625+
fmt.Printf(" %-14s %s\n", "", a.Usage)
5626+
fmt.Printf(" %-14s config: %s%s\n", "", a.Config, status)
5627+
fmt.Println()
5628+
}
5629+
}
5630+
5631+
// cmdTui launches the interactive service-agent TUI.
5632+
func cmdTui(args []string) {
5633+
// Locate tui.py: check next to the binary first, then fall back to ~/.pilot/tui.py.
5634+
exe, _ := os.Executable()
5635+
candidates := []string{
5636+
filepath.Join(filepath.Dir(exe), "tui.py"),
5637+
}
5638+
home, err := os.UserHomeDir()
5639+
if err == nil {
5640+
candidates = append(candidates, filepath.Join(home, ".pilot", "tui.py"))
5641+
}
5642+
5643+
var tuiPath string
5644+
for _, p := range candidates {
5645+
if _, err := os.Stat(p); err == nil {
5646+
tuiPath = p
5647+
break
5648+
}
5649+
}
5650+
if tuiPath == "" {
5651+
fatalHint("not_found",
5652+
"ensure tui.py is installed at ~/.pilot/tui.py or next to the pilotctl binary",
5653+
"TUI not found")
5654+
}
5655+
5656+
// Find python3.
5657+
python := "python3"
5658+
if p, err := exec.LookPath("python3"); err == nil {
5659+
python = p
5660+
} else if p, err := exec.LookPath("python"); err == nil {
5661+
python = p
5662+
} else {
5663+
fatalHint("not_found",
5664+
"install Python 3: https://python.org",
5665+
"python3 not found on PATH")
5666+
}
5667+
5668+
cmd := exec.Command(python, tuiPath)
5669+
cmd.Stdin = os.Stdin
5670+
cmd.Stdout = os.Stdout
5671+
cmd.Stderr = os.Stderr
5672+
if err := cmd.Run(); err != nil {
5673+
if exitErr, ok := err.(*exec.ExitError); ok {
5674+
os.Exit(exitErr.ExitCode())
5675+
}
5676+
fatalCode("internal", "tui: %v", err)
5677+
}
5678+
}
5679+
55325680
// loadClawditNode reads the target node from ~/.pilot/clawdit.yaml.
55335681
func loadClawditNode() string {
55345682
return loadPilotYAMLNode("clawdit.yaml")

0 commit comments

Comments
 (0)