Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions cmd/pilotctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,21 @@ Reliability caveats (current implementation):
// falls through pilotctl's per-command help intercept and
// prints "No specific help" — confusing for an RC-shipped CLI.
"appstore": AppStoreHelpText,

"review": `Usage: pilotctl review <pilot|app-id> [--rating 1-5] [--text "..."]

Submit a review or rating for Pilot Protocol or an installed app.
Feedback is sent to the Feedback agent (node 16437), which
aggregates it for the telemetry server (kind=review events).

Flags:
--rating 1-5 numeric rating (star count)
--text "..." free-form review text (max 4096 chars)

Examples:
pilotctl review pilot --rating 5 --text "Great protocol, very fast"
pilotctl review my-app --rating 3
`,
}

// printCommandHelp prints the help text for a command and exits.
Expand Down Expand Up @@ -1314,6 +1329,7 @@ Diagnostic commands:
pilotctl listen <port> [--count <n>] [--timeout <dur>]
pilotctl broadcast <network_id> <message>
pilotctl updates [--count <n>] [--scope <scope>] read https://teoslayer.github.io/pilot-changelog/feed.xml
pilotctl review <pilot|app-id> [--rating <n>] [--text "..."] submit a review

Agent tool discovery:
pilotctl context
Expand Down Expand Up @@ -1420,6 +1436,10 @@ dispatch:

case "appstore":
cmdAppStore(cmdArgs)

case "review":
cmdReview(cmdArgs)

return

// Bootstrap
Expand Down Expand Up @@ -2044,6 +2064,13 @@ func contextCatalog() map[string]interface{} {
"returns": "target, topic, bytes",
},

// Reviews
"review": map[string]interface{}{
"args": []string{"<pilot|app-id>", "[--rating 1-5]", "[--text ...]"},
"description": "Submit a rating/review for Pilot Protocol or an installed app",
"returns": "target, rating, text, to_node",
},

// Diagnostics
"info": map[string]interface{}{
"args": []string{},
Expand Down
148 changes: 148 additions & 0 deletions cmd/pilotctl/review.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

package main

import (
"encoding/json"
"fmt"
"strings"
"time"

"github.com/pilot-protocol/common/protocol"
"github.com/pilot-protocol/trustedagents"
)

// reviewPayload is the JSON body sent to the feedback agent.
type reviewPayload struct {
Kind string `json:"kind"` // always "review"
Target string `json:"target"` // "pilot" or the app identifier
Rating int `json:"rating,omitempty"` // 1–5, 0 = unrated
Text string `json:"text,omitempty"` // free-form review text
Timestamp int64 `json:"timestamp,omitempty"` // unix epoch seconds
}

const (
reviewFeedbackHostname = "feedback"
reviewFeedbackNodeID = 16437
)

// cmdReview dispatches the `pilotctl review` command.
//
// Usage: pilotctl review <pilot|app-id> [--rating 1-5] [--text "..."]
//
// Sends a structured review event as a datagram to the Feedback agent
// (node 16437, hostname "feedback"). The feedback agent forwards the
// review to the telemetry pipeline where it is persisted as a signed
// "kind=review" event.
func cmdReview(args []string) {
flags, pos := parseFlags(args)

if len(pos) < 1 {
fatalCode("invalid_argument",
"usage: pilotctl review <pilot|app-id> [--rating 1-5] [--text \"...\"]")
}

target := strings.TrimSpace(pos[0])
if target == "" {
fatalCode("invalid_argument", "review target cannot be empty")
}

rating := flagInt(flags, "rating", 0)
text := flagString(flags, "text", "")

if rating < 0 || rating > 5 {
fatalCode("invalid_argument", "--rating must be between 1 and 5 (or 0 to omit)")
}
if text != "" {
text = strings.TrimSpace(text)
if len(text) > 4096 {
fatalCode("invalid_argument", "review text must not exceed 4096 characters")
}
}

if rating == 0 && text == "" {
fatalCode("invalid_argument",
"provide at least one of --rating or --text")
}

// Build the review payload
payload := reviewPayload{
Kind: "review",
Target: target,
Rating: rating,
Text: text,
Timestamp: time.Now().Unix(),
}

body, err := json.Marshal(payload)
if err != nil {
fatalCode("internal", "marshal review: %v", err)
}

// Connect to the daemon and resolve the feedback agent.
d := connectDriver()
defer d.Close()

// Find the feedback agent — first from trusted-agents list, then
// fall back to hostname resolution.
var feedbackAddr protocol.Addr
allAgents := trustedagents.All()
var agent *trustedagents.Agent
for i := range allAgents {
if allAgents[i].NodeID == reviewFeedbackNodeID || allAgents[i].Hostname == reviewFeedbackHostname {
agent = &allAgents[i]
break
}
}
if agent == nil {
result, rerr := d.ResolveHostname(reviewFeedbackHostname)
if rerr != nil {
fatalHint("not_found",
"ensure the daemon is running and has reached the registry",
"cannot find the feedback agent: %v", rerr)
}
addrStr, _ := result["address"].(string)
fa, perr := protocol.ParseAddr(addrStr)
if perr != nil {
fatalCode("connection_failed", "parse feedback address %q: %v", addrStr, perr)
}
feedbackAddr = fa
} else {
addr, perr := protocol.ParseAddr(agent.Address)
if perr != nil {
fatalCode("internal", "parse trusted agent address %q: %v", agent.Address, perr)
}
feedbackAddr = addr
}

// Auto-handshake: the feedback agent is in the trusted-agents list,
// so the daemon auto-approves on first contact.
maybeAutoHandshake(d, feedbackAddr, false)

// Send the review as a datagram (fire-and-forget) on STDI/O port.
if err := d.SendTo(feedbackAddr, protocol.PortStdIO, body); err != nil {
fatalCode("connection_failed", "send review: %v", err)
}

if jsonOutput {
outputOK(map[string]interface{}{
"target": target,
"rating": rating,
"text": text,
"to_node": int(reviewFeedbackNodeID),
})
return
}

// Human output
fmt.Printf("review submitted for %s → feedback agent (node %d)\n", target, reviewFeedbackNodeID)
if rating > 0 {
if text != "" {
fmt.Printf(" rating: %d/5, text: %q\n", rating, text)
} else {
fmt.Printf(" rating: %d/5\n", rating)
}
} else if text != "" {
fmt.Printf(" text: %q\n", text)
}
}
1 change: 1 addition & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Diagnostic commands:
pilotctl listen <port> [--count <n>] [--timeout <dur>]
pilotctl broadcast <network_id> <message>
pilotctl updates [--count <n>] [--scope <scope>] read https://teoslayer.github.io/pilot-changelog/feed.xml
pilotctl review <pilot|app-id> [--rating <n>] [--text "..."] submit a review

Agent tool discovery:
pilotctl context
Expand Down
Loading