ScuttleBot

feat: scuttlectl CLI + npm wrapper All v0 commands implemented against the REST API: status, agents list, agent get/register/revoke/rotate Global --url/--token flags + SCUTTLEBOT_URL/TOKEN env vars. --json flag outputs raw indented JSON on every command. channels list + logs tail stubbed with clear "not yet" messages. sdk/npm/scuttlectl: @conflicthq/scuttlectl npm package wrapping the Go binary; auto-downloads correct platform binary on first npx run. Closes #10

lmata 2026-03-31 05:20 trunk
Commit 4c4aa94ec6fdd0170b2e8ea931df224c8f30f57a126425b37d3d6be62784c251
--- a/cmd/scuttlectl/internal/apiclient/apiclient.go
+++ b/cmd/scuttlectl/internal/apiclient/apiclient.go
@@ -0,0 +1,48 @@
1
+// Package apiclient is a minimal HTTP client for the scuttlebot REST API.
2
+package apiclient
3
+
4
+import (
5
+ "bytes"
6
+ "encoding/json"
7
+ "fmt"
8
+ "io"
9
+ "net/http"
10
+io"
11
+ "net/http"
12
+ "strings"
13
+)
14
+
15
+// Client calls the scuttlebot REST API.
16
+type Client struct {
17
+ base string
18
+ token string
19
+ http *http.Client
20
+}
21
+
22
+// New creates a Client targeting baseURL (e.g. "http://localhost:8080") with
23
+// the given bearer token.
24
+func New(baseURL, token string) *Client {
25
+ return &Client{base: baseURL, token: token, http: &http.Client{}}
26
+}
27
+
28
+// Status returns the raw JSON bytes from GET /v1/status.
29
+func (c *Client) Status() (json.RawMessage, error) {
30
+ return c.get("/v1/status")
31
+}
32
+
33
+// ListAgents returns the raw JSON bytes from GET /v1/agents.
34
+func (c *Cli}.
35
+func (c *Client) GetAgent(nick string) (json.RawMessage, error) {
36
+ return c.get("/v1/agents/" + nick)
37
+}
38
+
39
+// RegisterAgent sends POST /v1/agents/register and returns raw JSON.
40
+func (c *Client) RegisterAgent(nick, agentType string, channels []string) (json.RawMessage, error) {
41
+ body := map[string]any{"nick": nick}
42
+ if agentType != "" {
43
+ body["type"] = agentType
44
+ }
45
+ if len(channels) > 0 {
46
+ body["channels"] = channels
47
+ }
48
+ return c.post("/v1/agents/regi
--- a/cmd/scuttlectl/internal/apiclient/apiclient.go
+++ b/cmd/scuttlectl/internal/apiclient/apiclient.go
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/cmd/scuttlectl/internal/apiclient/apiclient.go
+++ b/cmd/scuttlectl/internal/apiclient/apiclient.go
@@ -0,0 +1,48 @@
1 // Package apiclient is a minimal HTTP client for the scuttlebot REST API.
2 package apiclient
3
4 import (
5 "bytes"
6 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10 io"
11 "net/http"
12 "strings"
13 )
14
15 // Client calls the scuttlebot REST API.
16 type Client struct {
17 base string
18 token string
19 http *http.Client
20 }
21
22 // New creates a Client targeting baseURL (e.g. "http://localhost:8080") with
23 // the given bearer token.
24 func New(baseURL, token string) *Client {
25 return &Client{base: baseURL, token: token, http: &http.Client{}}
26 }
27
28 // Status returns the raw JSON bytes from GET /v1/status.
29 func (c *Client) Status() (json.RawMessage, error) {
30 return c.get("/v1/status")
31 }
32
33 // ListAgents returns the raw JSON bytes from GET /v1/agents.
34 func (c *Cli}.
35 func (c *Client) GetAgent(nick string) (json.RawMessage, error) {
36 return c.get("/v1/agents/" + nick)
37 }
38
39 // RegisterAgent sends POST /v1/agents/register and returns raw JSON.
40 func (c *Client) RegisterAgent(nick, agentType string, channels []string) (json.RawMessage, error) {
41 body := map[string]any{"nick": nick}
42 if agentType != "" {
43 body["type"] = agentType
44 }
45 if len(channels) > 0 {
46 body["channels"] = channels
47 }
48 return c.post("/v1/agents/regi
--- cmd/scuttlectl/main.go
+++ cmd/scuttlectl/main.go
@@ -1,9 +1,321 @@
1
+// scuttlectl is the CLI for observing and managing a scuttlebot instance.
2
+//
3
+// Usage:
4
+//
5
+// scuttlectl [--url URL] [--token TOKEN] [--json] <command> [args]
6
+//
7
+// Environment variables:
8
+//
9
+// SCUTTLEBOT_URL API base URL (default: http://localhost:8080)
10
+// SCUTTLEBOT_TOKEN API bearer token
111
package main
212
3
-import "fmt"
13
+import (
14
+ "encoding/json"
15
+ "flag"
16
+ "fmt"
17
+ "os"
18
+ "strings"
19
+ "text/tabwriter"
20
+
21
+ "github.com/conflicthq/scuttlebot/cmd/scuttlectl/internal/apiclient"
22
+)
423
524
var version = "dev"
625
726
func main() {
8
- fmt.Printf("scuttlectl %s\n", version)
27
+ // Global flags.
28
+ urlFlag := flag.String("url", envOr("SCUTTLEBOT_URL", "http://localhost:8080"), "scuttlebot API base URL")
29
+ tokenFlag := flag.String("token", os.Getenv("SCUTTLEBOT_TOKEN"), "API bearer token")
30
+ jsonFlag := flag.Bool("json", false, "output raw JSON")
31
+ versionFlag := flag.Bool("version", false, "print version and exit")
32
+ flag.Usage = usage
33
+ flag.Parse()
34
+
35
+ if *versionFlag {
36
+ fmt.Println(version)
37
+ return
38
+ }
39
+
40
+ args := flag.Args()
41
+ if len(args) == 0 {
42
+ usage()
43
+ os.Exit(1)
44
+ }
45
+
46
+ if *tokenFlag == "" {
47
+ fmt.Fprintln(os.Stderr, "error: API token required (set SCUTTLEBOT_TOKEN or use --token)")
48
+ os.Exit(1)
49
+ }
50
+
51
+ api := apiclient.New(*urlFlag, *tokenFlag)
52
+
53
+ switch args[0] {
54
+ case "status":
55
+ cmdStatus(api, *jsonFlag)
56
+ case "agents", "agent":
57
+ if len(args) < 2 {
58
+ fmt.Fprintf(os.Stderr, "usage: scuttlectl %s <subcommand>\n", args[0])
59
+ os.Exit(1)
60
+ }
61
+ switch args[1] {
62
+ case "list":
63
+ cmdAgentList(api, *jsonFlag)
64
+ case "get":
65
+ requireArgs(args, 3, "scuttlectl agent get <nick>")
66
+ cmdAgentGet(api, args[2], *jsonFlag)
67
+ case "register":
68
+ requireArgs(args, 3, "scuttlectl agent register <nick> [--type worker] [--channels #a,#b]")
69
+ cmdAgentRegister(api, args[2:], *jsonFlag)
70
+ case "revoke":
71
+ requireArgs(args, 3, "scuttlectl agent revoke <nick>")
72
+ cmdAgentRevoke(api, args[2])
73
+ case "rotate":
74
+ requireArgs(args, 3, "scuttlectl agent rotate <nick>")
75
+ cmdAgentRotate(api, args[2], *jsonFlag)
76
+ default:
77
+ fmt.Fprintf(os.Stderr, "unknown subcommand: %s\n", args[1])
78
+ os.Exit(1)
79
+ }
80
+ case "channels":
81
+ if len(args) < 2 || args[1] == "list" {
82
+ fmt.Fprintln(os.Stderr, "channels list: not yet implemented (requires #12 discovery)")
83
+ os.Exit(1)
84
+ }
85
+ fmt.Fprintf(os.Stderr, "unknown subcommand: channels %s\n", args[1])
86
+ os.Exit(1)
87
+ case "logs":
88
+ fmt.Fprintln(os.Stderr, "logs tail: not yet implemented (requires scribe HTTP endpoint)")
89
+ os.Exit(1)
90
+ default:
91
+ fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
92
+ usage()
93
+ os.Exit(1)
94
+ }
95
+}
96
+
97
+func cmdStatus(api *apiclient.Client, asJSON bool) {
98
+ raw, err := api.Status()
99
+ die(err)
100
+ if asJSON {
101
+ printJSON(raw)
102
+ return
103
+ }
104
+
105
+ var s struct {
106
+ Status string `json:"status"`
107
+ Uptime string `json:"uptime"`
108
+ Agents int `json:"agents"`
109
+ Started string `json:"started"`
110
+ }
111
+ must(json.Unmarshal(raw, &s))
112
+
113
+ tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
114
+ fmt.Fprintf(tw, "status\t%s\n", s.Status)
115
+ fmt.Fprintf(tw, "uptime\t%s\n", s.Uptime)
116
+ fmt.Fprintf(tw, "agents\t%d\n", s.Agents)
117
+ fmt.Fprintf(tw, "started\t%s\n", s.Started)
118
+ tw.Flush()
119
+}
120
+
121
+func cmdAgentList(api *apiclient.Client, asJSON bool) {
122
+ raw, err := api.ListAgents()
123
+ die(err)
124
+ if asJSON {
125
+ printJSON(raw)
126
+ return
127
+ }
128
+
129
+ var body struct {
130
+ Agents []struct {
131
+ Nick string `json:"nick"`
132
+ Type string `json:"type"`
133
+ Channels []string `json:"channels"`
134
+ Revoked bool `json:"revoked"`
135
+ } `json:"agents"`
136
+ }
137
+ must(json.Unmarshal(raw, &body))
138
+
139
+ if len(body.Agents) == 0 {
140
+ fmt.Println("no agents registered")
141
+ return
142
+ }
143
+
144
+ tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
145
+ fmt.Fprintln(tw, "NICK\tTYPE\tCHANNELS\tSTATUS")
146
+ for _, a := range body.Agents {
147
+ status := "active"
148
+ if a.Revoked {
149
+ status = "revoked"
150
+ }
151
+ fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", a.Nick, a.Type, strings.Join(a.Channels, ","), status)
152
+ }
153
+ tw.Flush()
154
+}
155
+
156
+func cmdAgentGet(api *apiclient.Client, nick string, asJSON bool) {
157
+ raw, err := api.GetAgent(nick)
158
+ die(err)
159
+ if asJSON {
160
+ printJSON(raw)
161
+ return
162
+ }
163
+
164
+ var a struct {
165
+ Nick string `json:"nick"`
166
+ Type string `json:"type"`
167
+ Channels []string `json:"channels"`
168
+ Revoked bool `json:"revoked"`
169
+ }
170
+ must(json.Unmarshal(raw, &a))
171
+
172
+ tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
173
+ status := "active"
174
+ if a.Revoked {
175
+ status = "revoked"
176
+ }
177
+ fmt.Fprintf(tw, "nick\t%s\n", a.Nick)
178
+ fmt.Fprintf(tw, "type\t%s\n", a.Type)
179
+ fmt.Fprintf(tw, "channels\t%s\n", strings.Join(a.Channels, ", "))
180
+ fmt.Fprintf(tw, "status\t%s\n", status)
181
+ tw.Flush()
182
+}
183
+
184
+func cmdAgentRegister(api *apiclient.Client, args []string, asJSON bool) {
185
+ nick := args[0]
186
+ agentType := "worker"
187
+ var channels []string
188
+
189
+ // Parse optional --type and --channels from remaining args.
190
+ fs := flag.NewFlagSet("agent register", flag.ExitOnError)
191
+ typeFlag := fs.String("type", "worker", "agent type (worker, orchestrator, observer)")
192
+ channelsFlag := fs.String("channels", "", "comma-separated list of channels to join")
193
+ _ = fs.Parse(args[1:])
194
+ agentType = *typeFlag
195
+ if *channelsFlag != "" {
196
+ for _, ch := range strings.Split(*channelsFlag, ",") {
197
+ if ch = strings.TrimSpace(ch); ch != "" {
198
+ channels = append(channels, ch)
199
+ }
200
+ }
201
+ }
202
+
203
+ raw, err := api.RegisterAgent(nick, agentType, channels)
204
+ die(err)
205
+ if asJSON {
206
+ printJSON(raw)
207
+ return
208
+ }
209
+
210
+ var body struct {
211
+ Credentials struct {
212
+ Nick string `json:"nick"`
213
+ Password string `json:"password"`
214
+ Server string `json:"server"`
215
+ } `json:"credentials"`
216
+ Payload struct {
217
+ Token string `json:"token"`
218
+ Signature string `json:"signature"`
219
+ } `json:"payload"`
220
+ }
221
+ must(json.Unmarshal(raw, &body))
222
+
223
+ fmt.Printf("Agent registered: %s\n\n", nick)
224
+ tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
225
+ fmt.Fprintln(tw, "CREDENTIAL\tVALUE")
226
+ fmt.Fprintf(tw, "nick\t%s\n", body.Credentials.Nick)
227
+ fmt.Fprintf(tw, "password\t%s\n", body.Credentials.Password)
228
+ fmt.Fprintf(tw, "server\t%s\n", body.Credentials.Server)
229
+ tw.Flush()
230
+ fmt.Println("\nStore these credentials — the password will not be shown again.")
231
+}
232
+
233
+func cmdAgentRevoke(api *apiclient.Client, nick string) {
234
+ die(api.RevokeAgent(nick))
235
+ fmt.Printf("Agent revoked: %s\n", nick)
236
+}
237
+
238
+func cmdAgentRotate(api *apiclient.Client, nick string, asJSON bool) {
239
+ raw, err := api.RotateAgent(nick)
240
+ die(err)
241
+ if asJSON {
242
+ printJSON(raw)
243
+ return
244
+ }
245
+
246
+ var creds struct {
247
+ Nick string `json:"nick"`
248
+ Password string `json:"password"`
249
+ Server string `json:"server"`
250
+ }
251
+ must(json.Unmarshal(raw, &creds))
252
+
253
+ fmt.Printf("Credentials rotated for: %s\n\n", nick)
254
+ tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
255
+ fmt.Fprintln(tw, "CREDENTIAL\tVALUE")
256
+ fmt.Fprintf(tw, "nick\t%s\n", creds.Nick)
257
+ fmt.Fprintf(tw, "password\t%s\n", creds.Password)
258
+ fmt.Fprintf(tw, "server\t%s\n", creds.Server)
259
+ tw.Flush()
260
+ fmt.Println("\nStore this password — it will not be shown again.")
261
+}
262
+
263
+func usage() {
264
+ fmt.Fprintf(os.Stderr, `scuttlectl %s — scuttlebot management CLI
265
+
266
+Usage:
267
+ scuttlectl [flags] <command> [subcommand] [args]
268
+
269
+Global flags:
270
+ --url API base URL (default: $SCUTTLEBOT_URL or http://localhost:8080)
271
+ --token API bearer token (default: $SCUTTLEBOT_TOKEN)
272
+ --json output raw JSON
273
+ --version print version and exit
274
+
275
+Commands:
276
+ status daemon + ergo health
277
+ agents list list all registered agents
278
+ agent get <nick> get a single agent
279
+ agent register <nick> register a new agent, print credentials
280
+ [--type worker|orchestrator|observer]
281
+ [--channels #a,#b,#c]
282
+ agent revoke <nick> revoke agent credentials
283
+ agent rotate <nick> rotate agent password
284
+ channels list list provisioned channels (requires #12)
285
+ logs tail tail scribe log (coming soon)
286
+`, version)
287
+}
288
+
289
+func printJSON(raw json.RawMessage) {
290
+ var buf []byte
291
+ buf, _ = json.MarshalIndent(raw, "", " ")
292
+ fmt.Println(string(buf))
293
+}
294
+
295
+func requireArgs(args []string, n int, usage string) {
296
+ if len(args) < n {
297
+ fmt.Fprintf(os.Stderr, "usage: %s\n", usage)
298
+ os.Exit(1)
299
+ }
300
+}
301
+
302
+func die(err error) {
303
+ if err != nil {
304
+ fmt.Fprintln(os.Stderr, "error:", err)
305
+ os.Exit(1)
306
+ }
307
+}
308
+
309
+func must(err error) {
310
+ if err != nil {
311
+ fmt.Fprintln(os.Stderr, "internal error:", err)
312
+ os.Exit(1)
313
+ }
314
+}
315
+
316
+func envOr(key, def string) string {
317
+ if v := os.Getenv(key); v != "" {
318
+ return v
319
+ }
320
+ return def
9321
}
10322
11323
ADDED sdk/npm/scuttlectl/bin/scuttlectl.js
12324
ADDED sdk/npm/scuttlectl/package.json
--- cmd/scuttlectl/main.go
+++ cmd/scuttlectl/main.go
@@ -1,9 +1,321 @@
 
 
 
 
 
 
 
 
 
 
1 package main
2
3 import "fmt"
 
 
 
 
 
 
 
 
 
4
5 var version = "dev"
6
7 func main() {
8 fmt.Printf("scuttlectl %s\n", version)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9 }
10
11 DDED sdk/npm/scuttlectl/bin/scuttlectl.js
12 DDED sdk/npm/scuttlectl/package.json
--- cmd/scuttlectl/main.go
+++ cmd/scuttlectl/main.go
@@ -1,9 +1,321 @@
1 // scuttlectl is the CLI for observing and managing a scuttlebot instance.
2 //
3 // Usage:
4 //
5 // scuttlectl [--url URL] [--token TOKEN] [--json] <command> [args]
6 //
7 // Environment variables:
8 //
9 // SCUTTLEBOT_URL API base URL (default: http://localhost:8080)
10 // SCUTTLEBOT_TOKEN API bearer token
11 package main
12
13 import (
14 "encoding/json"
15 "flag"
16 "fmt"
17 "os"
18 "strings"
19 "text/tabwriter"
20
21 "github.com/conflicthq/scuttlebot/cmd/scuttlectl/internal/apiclient"
22 )
23
24 var version = "dev"
25
26 func main() {
27 // Global flags.
28 urlFlag := flag.String("url", envOr("SCUTTLEBOT_URL", "http://localhost:8080"), "scuttlebot API base URL")
29 tokenFlag := flag.String("token", os.Getenv("SCUTTLEBOT_TOKEN"), "API bearer token")
30 jsonFlag := flag.Bool("json", false, "output raw JSON")
31 versionFlag := flag.Bool("version", false, "print version and exit")
32 flag.Usage = usage
33 flag.Parse()
34
35 if *versionFlag {
36 fmt.Println(version)
37 return
38 }
39
40 args := flag.Args()
41 if len(args) == 0 {
42 usage()
43 os.Exit(1)
44 }
45
46 if *tokenFlag == "" {
47 fmt.Fprintln(os.Stderr, "error: API token required (set SCUTTLEBOT_TOKEN or use --token)")
48 os.Exit(1)
49 }
50
51 api := apiclient.New(*urlFlag, *tokenFlag)
52
53 switch args[0] {
54 case "status":
55 cmdStatus(api, *jsonFlag)
56 case "agents", "agent":
57 if len(args) < 2 {
58 fmt.Fprintf(os.Stderr, "usage: scuttlectl %s <subcommand>\n", args[0])
59 os.Exit(1)
60 }
61 switch args[1] {
62 case "list":
63 cmdAgentList(api, *jsonFlag)
64 case "get":
65 requireArgs(args, 3, "scuttlectl agent get <nick>")
66 cmdAgentGet(api, args[2], *jsonFlag)
67 case "register":
68 requireArgs(args, 3, "scuttlectl agent register <nick> [--type worker] [--channels #a,#b]")
69 cmdAgentRegister(api, args[2:], *jsonFlag)
70 case "revoke":
71 requireArgs(args, 3, "scuttlectl agent revoke <nick>")
72 cmdAgentRevoke(api, args[2])
73 case "rotate":
74 requireArgs(args, 3, "scuttlectl agent rotate <nick>")
75 cmdAgentRotate(api, args[2], *jsonFlag)
76 default:
77 fmt.Fprintf(os.Stderr, "unknown subcommand: %s\n", args[1])
78 os.Exit(1)
79 }
80 case "channels":
81 if len(args) < 2 || args[1] == "list" {
82 fmt.Fprintln(os.Stderr, "channels list: not yet implemented (requires #12 discovery)")
83 os.Exit(1)
84 }
85 fmt.Fprintf(os.Stderr, "unknown subcommand: channels %s\n", args[1])
86 os.Exit(1)
87 case "logs":
88 fmt.Fprintln(os.Stderr, "logs tail: not yet implemented (requires scribe HTTP endpoint)")
89 os.Exit(1)
90 default:
91 fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
92 usage()
93 os.Exit(1)
94 }
95 }
96
97 func cmdStatus(api *apiclient.Client, asJSON bool) {
98 raw, err := api.Status()
99 die(err)
100 if asJSON {
101 printJSON(raw)
102 return
103 }
104
105 var s struct {
106 Status string `json:"status"`
107 Uptime string `json:"uptime"`
108 Agents int `json:"agents"`
109 Started string `json:"started"`
110 }
111 must(json.Unmarshal(raw, &s))
112
113 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
114 fmt.Fprintf(tw, "status\t%s\n", s.Status)
115 fmt.Fprintf(tw, "uptime\t%s\n", s.Uptime)
116 fmt.Fprintf(tw, "agents\t%d\n", s.Agents)
117 fmt.Fprintf(tw, "started\t%s\n", s.Started)
118 tw.Flush()
119 }
120
121 func cmdAgentList(api *apiclient.Client, asJSON bool) {
122 raw, err := api.ListAgents()
123 die(err)
124 if asJSON {
125 printJSON(raw)
126 return
127 }
128
129 var body struct {
130 Agents []struct {
131 Nick string `json:"nick"`
132 Type string `json:"type"`
133 Channels []string `json:"channels"`
134 Revoked bool `json:"revoked"`
135 } `json:"agents"`
136 }
137 must(json.Unmarshal(raw, &body))
138
139 if len(body.Agents) == 0 {
140 fmt.Println("no agents registered")
141 return
142 }
143
144 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
145 fmt.Fprintln(tw, "NICK\tTYPE\tCHANNELS\tSTATUS")
146 for _, a := range body.Agents {
147 status := "active"
148 if a.Revoked {
149 status = "revoked"
150 }
151 fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", a.Nick, a.Type, strings.Join(a.Channels, ","), status)
152 }
153 tw.Flush()
154 }
155
156 func cmdAgentGet(api *apiclient.Client, nick string, asJSON bool) {
157 raw, err := api.GetAgent(nick)
158 die(err)
159 if asJSON {
160 printJSON(raw)
161 return
162 }
163
164 var a struct {
165 Nick string `json:"nick"`
166 Type string `json:"type"`
167 Channels []string `json:"channels"`
168 Revoked bool `json:"revoked"`
169 }
170 must(json.Unmarshal(raw, &a))
171
172 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
173 status := "active"
174 if a.Revoked {
175 status = "revoked"
176 }
177 fmt.Fprintf(tw, "nick\t%s\n", a.Nick)
178 fmt.Fprintf(tw, "type\t%s\n", a.Type)
179 fmt.Fprintf(tw, "channels\t%s\n", strings.Join(a.Channels, ", "))
180 fmt.Fprintf(tw, "status\t%s\n", status)
181 tw.Flush()
182 }
183
184 func cmdAgentRegister(api *apiclient.Client, args []string, asJSON bool) {
185 nick := args[0]
186 agentType := "worker"
187 var channels []string
188
189 // Parse optional --type and --channels from remaining args.
190 fs := flag.NewFlagSet("agent register", flag.ExitOnError)
191 typeFlag := fs.String("type", "worker", "agent type (worker, orchestrator, observer)")
192 channelsFlag := fs.String("channels", "", "comma-separated list of channels to join")
193 _ = fs.Parse(args[1:])
194 agentType = *typeFlag
195 if *channelsFlag != "" {
196 for _, ch := range strings.Split(*channelsFlag, ",") {
197 if ch = strings.TrimSpace(ch); ch != "" {
198 channels = append(channels, ch)
199 }
200 }
201 }
202
203 raw, err := api.RegisterAgent(nick, agentType, channels)
204 die(err)
205 if asJSON {
206 printJSON(raw)
207 return
208 }
209
210 var body struct {
211 Credentials struct {
212 Nick string `json:"nick"`
213 Password string `json:"password"`
214 Server string `json:"server"`
215 } `json:"credentials"`
216 Payload struct {
217 Token string `json:"token"`
218 Signature string `json:"signature"`
219 } `json:"payload"`
220 }
221 must(json.Unmarshal(raw, &body))
222
223 fmt.Printf("Agent registered: %s\n\n", nick)
224 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
225 fmt.Fprintln(tw, "CREDENTIAL\tVALUE")
226 fmt.Fprintf(tw, "nick\t%s\n", body.Credentials.Nick)
227 fmt.Fprintf(tw, "password\t%s\n", body.Credentials.Password)
228 fmt.Fprintf(tw, "server\t%s\n", body.Credentials.Server)
229 tw.Flush()
230 fmt.Println("\nStore these credentials — the password will not be shown again.")
231 }
232
233 func cmdAgentRevoke(api *apiclient.Client, nick string) {
234 die(api.RevokeAgent(nick))
235 fmt.Printf("Agent revoked: %s\n", nick)
236 }
237
238 func cmdAgentRotate(api *apiclient.Client, nick string, asJSON bool) {
239 raw, err := api.RotateAgent(nick)
240 die(err)
241 if asJSON {
242 printJSON(raw)
243 return
244 }
245
246 var creds struct {
247 Nick string `json:"nick"`
248 Password string `json:"password"`
249 Server string `json:"server"`
250 }
251 must(json.Unmarshal(raw, &creds))
252
253 fmt.Printf("Credentials rotated for: %s\n\n", nick)
254 tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
255 fmt.Fprintln(tw, "CREDENTIAL\tVALUE")
256 fmt.Fprintf(tw, "nick\t%s\n", creds.Nick)
257 fmt.Fprintf(tw, "password\t%s\n", creds.Password)
258 fmt.Fprintf(tw, "server\t%s\n", creds.Server)
259 tw.Flush()
260 fmt.Println("\nStore this password — it will not be shown again.")
261 }
262
263 func usage() {
264 fmt.Fprintf(os.Stderr, `scuttlectl %s — scuttlebot management CLI
265
266 Usage:
267 scuttlectl [flags] <command> [subcommand] [args]
268
269 Global flags:
270 --url API base URL (default: $SCUTTLEBOT_URL or http://localhost:8080)
271 --token API bearer token (default: $SCUTTLEBOT_TOKEN)
272 --json output raw JSON
273 --version print version and exit
274
275 Commands:
276 status daemon + ergo health
277 agents list list all registered agents
278 agent get <nick> get a single agent
279 agent register <nick> register a new agent, print credentials
280 [--type worker|orchestrator|observer]
281 [--channels #a,#b,#c]
282 agent revoke <nick> revoke agent credentials
283 agent rotate <nick> rotate agent password
284 channels list list provisioned channels (requires #12)
285 logs tail tail scribe log (coming soon)
286 `, version)
287 }
288
289 func printJSON(raw json.RawMessage) {
290 var buf []byte
291 buf, _ = json.MarshalIndent(raw, "", " ")
292 fmt.Println(string(buf))
293 }
294
295 func requireArgs(args []string, n int, usage string) {
296 if len(args) < n {
297 fmt.Fprintf(os.Stderr, "usage: %s\n", usage)
298 os.Exit(1)
299 }
300 }
301
302 func die(err error) {
303 if err != nil {
304 fmt.Fprintln(os.Stderr, "error:", err)
305 os.Exit(1)
306 }
307 }
308
309 func must(err error) {
310 if err != nil {
311 fmt.Fprintln(os.Stderr, "internal error:", err)
312 os.Exit(1)
313 }
314 }
315
316 func envOr(key, def string) string {
317 if v := os.Getenv(key); v != "" {
318 return v
319 }
320 return def
321 }
322
323 DDED sdk/npm/scuttlectl/bin/scuttlectl.js
324 DDED sdk/npm/scuttlectl/package.json
--- a/sdk/npm/scuttlectl/bin/scuttlectl.js
+++ b/sdk/npm/scuttlectl/bin/scuttlectl.js
@@ -0,0 +1,93 @@
1
+#!/usr/bin/env node
2
+/**
3
+ * scuttlectl npm wrapper.
4
+ * Downloads the appropriate scuttlectl binary for the current platform
5
+ * and executes it, passing all arguments through.
6
+ */
7
+"use strict";
8
+
9
+const { execFileSync } = require("child_process");
10
+const { createWriteStream, existsSync, mkdirSync, chmodSync } = require("fs");
11
+const { get } = require("https");
12
+const { join } = require("path");
13
+const { createGunzip } = require("zlib");
14
+const { Extract } = require("tar");
15
+
16
+const REPO = "ConflictHQ/scuttlebot";
17
+const BIN_DIR = join(__dirname, "..", "dist");
18
+const BIN_PATH = join(BIN_DIR, process.platform === "win32" ? "scuttlectl.exe" : "scuttlectl");
19
+
20
+function platformSuffix() {
21
+ const os = process.platform === "darwin" ? "darwin" : "linux";
22
+ const arch = process.arch === "arm64" ? "arm64" : "x86_64";
23
+ return `${os}-${arch}`;
24
+}
25
+
26
+async function fetchLatestVersion() {
27
+ return new Promise((resolve, reject) => {
28
+ get(
29
+ `https://api.github.com/repos/${REPO}/releases/latest`,
30
+ { headers: { "User-Agent": "scuttlectl-npm" } },
31
+ (res) => {
32
+ let data = "";
33
+ res.on("data", (c) => (data += c));
34
+ res.on("end", () => {
35
+ try {
36
+ resolve(JSON.parse(data).tag_name);
37
+ } catch (e) {
38
+ reject(e);
39
+ }
40
+ });
41
+ }
42
+ ).on("error", reject);
43
+ });
44
+}
45
+
46
+async function download(url, dest) {
47
+ return new Promise((resolve, reject) => {
48
+ get(url, { headers: { "User-Agent": "scuttlectl-npm" } }, (res) => {
49
+ if (res.statusCode === 302 || res.statusCode === 301) {
50
+ return download(res.headers.location, dest).then(resolve).catch(reject);
51
+ }
52
+ mkdirSync(BIN_DIR, { recursive: true });
53
+ const out = createWriteStream(dest);
54
+ res.pipe(out);
55
+ out.on("finish", resolve);
56
+ out.on("error", reject);
57
+ }).on("error", reject);
58
+ });
59
+}
60
+
61
+async function ensureBinary() {
62
+ if (existsSync(BIN_PATH)) return;
63
+
64
+ const version = await fetchLatestVersion();
65
+ const suffix = platformSuffix();
66
+ const asset = `scuttlectl-${version}-${suffix}.tar.gz`;
67
+ const url = `https://github.com/${REPO}/releases/download/${version}/${asset}`;
68
+ const tarPath = join(BIN_DIR, asset);
69
+
70
+ process.stderr.write(`Downloading scuttlectl ${version}...\n`);
71
+ await download(url, tarPath);
72
+
73
+ await new Promise((resolve, reject) => {
74
+ require("fs")
75
+ .createReadStream(tarPath)
76
+ .pipe(createGunzip())
77
+ .pipe(Extract({ cwd: BIN_DIR, strip: 0 }))
78
+ .on("finish", resolve)
79
+ .on("error", reject);
80
+ });
81
+
82
+ chmodSync(BIN_PATH, 0o755);
83
+ require("fs").unlinkSync(tarPath);
84
+}
85
+
86
+ensureBinary()
87
+ .then(() => {
88
+ execFileSync(BIN_PATH, process.argv.slice(2), { stdio: "inherit" });
89
+ })
90
+ .catch((err) => {
91
+ process.stderr.write(`scuttlectl: ${err.message}\n`);
92
+ process.exit(1);
93
+ });
--- a/sdk/npm/scuttlectl/bin/scuttlectl.js
+++ b/sdk/npm/scuttlectl/bin/scuttlectl.js
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/sdk/npm/scuttlectl/bin/scuttlectl.js
+++ b/sdk/npm/scuttlectl/bin/scuttlectl.js
@@ -0,0 +1,93 @@
1 #!/usr/bin/env node
2 /**
3 * scuttlectl npm wrapper.
4 * Downloads the appropriate scuttlectl binary for the current platform
5 * and executes it, passing all arguments through.
6 */
7 "use strict";
8
9 const { execFileSync } = require("child_process");
10 const { createWriteStream, existsSync, mkdirSync, chmodSync } = require("fs");
11 const { get } = require("https");
12 const { join } = require("path");
13 const { createGunzip } = require("zlib");
14 const { Extract } = require("tar");
15
16 const REPO = "ConflictHQ/scuttlebot";
17 const BIN_DIR = join(__dirname, "..", "dist");
18 const BIN_PATH = join(BIN_DIR, process.platform === "win32" ? "scuttlectl.exe" : "scuttlectl");
19
20 function platformSuffix() {
21 const os = process.platform === "darwin" ? "darwin" : "linux";
22 const arch = process.arch === "arm64" ? "arm64" : "x86_64";
23 return `${os}-${arch}`;
24 }
25
26 async function fetchLatestVersion() {
27 return new Promise((resolve, reject) => {
28 get(
29 `https://api.github.com/repos/${REPO}/releases/latest`,
30 { headers: { "User-Agent": "scuttlectl-npm" } },
31 (res) => {
32 let data = "";
33 res.on("data", (c) => (data += c));
34 res.on("end", () => {
35 try {
36 resolve(JSON.parse(data).tag_name);
37 } catch (e) {
38 reject(e);
39 }
40 });
41 }
42 ).on("error", reject);
43 });
44 }
45
46 async function download(url, dest) {
47 return new Promise((resolve, reject) => {
48 get(url, { headers: { "User-Agent": "scuttlectl-npm" } }, (res) => {
49 if (res.statusCode === 302 || res.statusCode === 301) {
50 return download(res.headers.location, dest).then(resolve).catch(reject);
51 }
52 mkdirSync(BIN_DIR, { recursive: true });
53 const out = createWriteStream(dest);
54 res.pipe(out);
55 out.on("finish", resolve);
56 out.on("error", reject);
57 }).on("error", reject);
58 });
59 }
60
61 async function ensureBinary() {
62 if (existsSync(BIN_PATH)) return;
63
64 const version = await fetchLatestVersion();
65 const suffix = platformSuffix();
66 const asset = `scuttlectl-${version}-${suffix}.tar.gz`;
67 const url = `https://github.com/${REPO}/releases/download/${version}/${asset}`;
68 const tarPath = join(BIN_DIR, asset);
69
70 process.stderr.write(`Downloading scuttlectl ${version}...\n`);
71 await download(url, tarPath);
72
73 await new Promise((resolve, reject) => {
74 require("fs")
75 .createReadStream(tarPath)
76 .pipe(createGunzip())
77 .pipe(Extract({ cwd: BIN_DIR, strip: 0 }))
78 .on("finish", resolve)
79 .on("error", reject);
80 });
81
82 chmodSync(BIN_PATH, 0o755);
83 require("fs").unlinkSync(tarPath);
84 }
85
86 ensureBinary()
87 .then(() => {
88 execFileSync(BIN_PATH, process.argv.slice(2), { stdio: "inherit" });
89 })
90 .catch((err) => {
91 process.stderr.write(`scuttlectl: ${err.message}\n`);
92 process.exit(1);
93 });
--- a/sdk/npm/scuttlectl/package.json
+++ b/sdk/npm/scuttlectl/package.json
@@ -0,0 +1,25 @@
1
+{
2
+ "name": "@conflicthq/scuttlectl",
3
+ "version": "0.0.1",
4
+ "description": "scuttlectl — CLI for managing scuttlebot instances",
5
+ "bin": {
6
+ "scuttlectl": "./bin/scuttlectl.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "README.md"
11
+ ],
12
+ "scripts": {},
13
+ "keywords": ["irc", "agents", "scuttlebot", "ai"],
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/ConflictHQ/scuttlebot"
18
+ },
19
+ "dependencies": {
20
+ "tar": "^6.2.1"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ }
25
+}
--- a/sdk/npm/scuttlectl/package.json
+++ b/sdk/npm/scuttlectl/package.json
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/sdk/npm/scuttlectl/package.json
+++ b/sdk/npm/scuttlectl/package.json
@@ -0,0 +1,25 @@
1 {
2 "name": "@conflicthq/scuttlectl",
3 "version": "0.0.1",
4 "description": "scuttlectl — CLI for managing scuttlebot instances",
5 "bin": {
6 "scuttlectl": "./bin/scuttlectl.js"
7 },
8 "files": [
9 "bin/",
10 "README.md"
11 ],
12 "scripts": {},
13 "keywords": ["irc", "agents", "scuttlebot", "ai"],
14 "license": "MIT",
15 "repository": {
16 "type": "git",
17 "url": "https://github.com/ConflictHQ/scuttlebot"
18 },
19 "dependencies": {
20 "tar": "^6.2.1"
21 },
22 "engines": {
23 "node": ">=18"
24 }
25 }

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button