Commit62f704baRecorded25 Mar 2026Repositorytube

Address style issues from code review

Message

- Replace hand-rolled find-flag/positional-args with (sigil args) library for declarative CLI parsing with subcommands, options, and auto-help - Optimize with-twitch-context usage in tool-analytics and tool-go-live to resolve broadcaster-id once instead of per API call - Document YouTube chat limitation in tube/chat MCP tool description - Document Twitch analytics asymmetry in tube/analytics MCP tool description

Changed
 src/tube/main.sgl   | 232 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------------------------------------
 src/tube/tools.sgl  |  43 +++++++++++++++++---------------
 src/tube/twitch.sgl |   1 +
 3 files changed, 150 insertions(+), 126 deletions(-)
Diff
src/tube/main.sglmodified
@@ -1,13 +1,14 @@
1
;;; (tube main) - Entry point for tube CLI and MCP server.
2
;;;
3
;;; Dispatches between MCP server mode (`tube serve`) and CLI mode
4
;;; (`tube <command> <args>`). Follows the fjo/tally dual-mode pattern.
+4
;;; (`tube <command> <args>`). Uses (sigil args) for CLI parsing.
5
6
(define-library (tube main)
7
(import (sigil core)
8
(sigil string)
9
(sigil dict)
10
(sigil process)
+11
(sigil args)
12
(sigil mcp server)
13
(tube config)
14
(tube youtube)
@@ -21,61 +22,26 @@
22
(register-tube-tools! server config)
23
(mcp-server-run server)))
24
24
(define (print-usage)
25
(display "tube - Video platform management tool (YouTube + Twitch)\n\n")
26
(display "Usage:\n")
27
(display " tube serve Start MCP server\n")
28
(display " tube upload <file> [--title T] [--description D] [--privacy P]\n")
29
(display " Upload video to YouTube\n")
30
(display " tube videos [--limit N] List recent YouTube videos\n")
31
(display " tube analytics [--days N] [--platform P] Analytics summary\n")
32
(display " tube stream-schedule [--platform P] Show upcoming streams\n")
33
(display " tube go-live [--title T] [--platform P] Start a broadcast\n")
34
(display " tube chat <message> [--platform P] Send chat message\n")
35
(display " tube status [--platform P] Platform overview\n\n")
36
(display "Platforms: youtube, twitch, both (default: both)\n\n")
37
(display "Environment:\n")
38
(display " YOUTUBE_ACCESS_TOKEN YouTube OAuth2 token\n")
39
(display " YOUTUBE_API_KEY YouTube API key (read-only)\n")
40
(display " TWITCH_CLIENT_ID Twitch application client ID\n")
41
(display " TWITCH_ACCESS_TOKEN Twitch OAuth2 token\n")
42
(display " TUBE_DEFAULT_PLATFORM Default platform (youtube/twitch/both)\n"))
43
44
;;; Find a --flag value in an argument list.
45
(define (find-flag args flag default)
46
(let loop ((rest args))
47
(cond
48
((null? rest) default)
49
((and (string=? (car rest) flag)
50
(not (null? (cdr rest))))
51
(cadr rest))
52
(else (loop (cdr rest))))))
53
54
;;; Collect non-flag arguments (positional args).
55
(define (positional-args args)
56
(let loop ((rest args) (acc '()))
57
(cond
58
((null? rest) (reverse acc))
59
((and (> (string-length (car rest)) 1)
60
(char=? (string-ref (car rest) 0) #\-)
61
(char=? (string-ref (car rest) 1) #\-))
62
;; Skip --flag and its value
63
(if (null? (cdr rest))
64
(reverse acc)
65
(loop (cddr rest) acc)))
66
(else
67
(loop (cdr rest) (cons (car rest) acc))))))
+25
;; ============================================================
+26
;; Shared option builders
+27
;; ============================================================
+28
+29
(define (platform-option default)
+30
(option name: 'platform short: #\p long: "platform"
+31
value: "PLATFORM"
+32
description: "Platform: youtube, twitch, or both"
+33
choices: '("youtube" "twitch" "both")
+34
default: default))
35
36
;; ============================================================
37
;; CLI Command Handlers
38
;; ============================================================
39
73
(define (cli-upload config args)
74
(let* ((positionals (positional-args args))
75
(file (if (pair? positionals) (car positionals) #f))
76
(title (find-flag args "--title" "Untitled"))
77
(description (find-flag args "--description" ""))
78
(privacy (find-flag args "--privacy" "private")))
+40
(define (cli-upload config opts args)
+41
(let ((file (if (pair? args) (car args) #f))
+42
(title (alist-get 'title opts))
+43
(description (alist-get 'description opts))
+44
(privacy (alist-get 'privacy opts)))
45
(unless file
46
(display "Error: file path is required.\n")
47
(exit 1))
@@ -89,8 +55,8 @@
55
"Video ID: " (youtube-video-id result) "\n"
56
"Privacy: " privacy "\n"))))))
57
92
(define (cli-videos config args)
93
(let* ((limit (string->number (find-flag args "--limit" "10")))
+58
(define (cli-videos config opts args)
+59
(let* ((limit (alist-get 'limit opts))
60
(videos (tube-list-videos config limit)))
61
(if (null? videos)
62
(display "No videos found.\n")
@@ -102,42 +68,36 @@
68
(display "\n\n"))
69
videos)))))
70
105
(define (cli-analytics config args start-default end-default
106
now-timestamp)
107
(let* ((days (find-flag args "--days" #f))
108
(platform (find-flag args "--platform"
109
(tube-config-default-platform config)))
+71
(define (cli-analytics config opts start-default end-default
+72
now-timestamp)
+73
(let* ((days (alist-get 'days opts))
+74
(platform (alist-get 'platform opts))
75
(params #{ platform: platform
76
end_date: end-default })
77
(params (if days
113
(dict-set params days: (string->number days))
+78
(dict-set params days: days)
79
params)))
80
;; Reuse the tool handler
81
(display (tool-analytics config params start-default end-default
82
now-timestamp))
83
(newline)))
84
120
(define (cli-schedule config args)
121
(let* ((platform (find-flag args "--platform"
122
(tube-config-default-platform config)))
123
(params #{ platform: platform }))
+85
(define (cli-schedule config opts)
+86
(let ((params #{ platform: (alist-get 'platform opts) }))
87
(display (tool-schedule config params))
88
(newline)))
89
127
(define (cli-go-live config args)
128
(let* ((title (find-flag args "--title" "Live Stream"))
129
(platform (find-flag args "--platform"
130
(tube-config-default-platform config)))
131
(params #{ title: title platform: platform }))
+90
(define (cli-go-live config opts)
+91
(let ((params #{ title: (alist-get 'title opts)
+92
platform: (alist-get 'platform opts) }))
93
(display (tool-go-live config params))
94
(newline)))
95
135
(define (cli-chat config args)
136
(let* ((positionals (positional-args args))
137
(message (if (pair? positionals)
138
(string-join positionals " ")
139
#f))
140
(platform (find-flag args "--platform" "twitch")))
+96
(define (cli-chat config opts args)
+97
(let ((message (if (pair? args)
+98
(string-join args " ")
+99
#f))
+100
(platform (alist-get 'platform opts)))
101
(unless message
102
(display "Error: chat message is required.\n")
103
(exit 1))
@@ -145,46 +105,104 @@
105
(display (tool-chat config params))
106
(newline))))
107
148
(define (cli-status config args)
149
(let* ((platform (find-flag args "--platform"
150
(tube-config-default-platform config)))
151
(params #{ platform: platform }))
+108
(define (cli-status config opts)
+109
(let ((params #{ platform: (alist-get 'platform opts) }))
110
(display (tool-status config params))
111
(newline)))
112
+113
;; ============================================================
+114
;; CLI Definition
+115
;; ============================================================
+116
+117
(define (make-cli config start-default end-default now-ts)
+118
(let ((default-plat (tube-config-default-platform config)))
+119
(command
+120
name: "tube"
+121
description: "Video platform management tool (YouTube + Twitch)"
+122
subcommands: (list
+123
(command
+124
name: "serve"
+125
description: "Start MCP server"
+126
handler: (lambda (opts args) (run-server config)))
+127
+128
(command
+129
name: "upload"
+130
description: "Upload video to YouTube"
+131
options: (list
+132
(option name: 'title short: #\t long: "title"
+133
value: "TITLE" default: "Untitled"
+134
description: "Video title")
+135
(option name: 'description short: #\d long: "description"
+136
value: "DESC" default: ""
+137
description: "Video description")
+138
(option name: 'privacy long: "privacy"
+139
value: "PRIVACY" default: "private"
+140
description: "Privacy status"
+141
choices: '("public" "private" "unlisted")))
+142
handler: (lambda (opts args) (cli-upload config opts args)))
+143
+144
(command
+145
name: "videos"
+146
description: "List recent YouTube videos"
+147
options: (list
+148
(option name: 'limit short: #\n long: "limit"
+149
value: "N" default: 10
+150
parse: string->number
+151
description: "Number of videos to return"))
+152
handler: (lambda (opts args) (cli-videos config opts args)))
+153
+154
(command
+155
name: "analytics"
+156
description: "Analytics summary"
+157
options: (list
+158
(option name: 'days long: "days"
+159
value: "N" default: #f
+160
parse: string->number
+161
description: "Number of days to look back")
+162
(platform-option default-plat))
+163
handler: (lambda (opts args)
+164
(cli-analytics config opts start-default
+165
end-default now-ts)))
+166
+167
(command
+168
name: "stream-schedule"
+169
description: "Show upcoming streams"
+170
options: (list (platform-option default-plat))
+171
handler: (lambda (opts args) (cli-schedule config opts)))
+172
+173
(command
+174
name: "go-live"
+175
description: "Start a broadcast"
+176
options: (list
+177
(option name: 'title short: #\t long: "title"
+178
value: "TITLE" default: "Live Stream"
+179
description: "Stream title")
+180
(platform-option default-plat))
+181
handler: (lambda (opts args) (cli-go-live config opts)))
+182
+183
(command
+184
name: "chat"
+185
description: "Send chat message"
+186
options: (list
+187
(platform-option "twitch"))
+188
handler: (lambda (opts args) (cli-chat config opts args)))
+189
+190
(command
+191
name: "status"
+192
description: "Platform overview"
+193
options: (list (platform-option default-plat))
+194
handler: (lambda (opts args) (cli-status config opts)))))))
+195
196
;; ============================================================
197
;; Entry Point
198
;; ============================================================
199
200
(define (main)
160
(let ((config (load-tube-config))
161
(args (cdr (command-line)))
162
(start-default (default-start-date))
163
(end-default (default-end-date))
164
(now-ts (default-now-timestamp)))
165
(cond
166
((null? args)
167
(print-usage)
168
(exit 1))
169
((string=? (car args) "serve")
170
(run-server config))
171
((string=? (car args) "upload")
172
(cli-upload config (cdr args)))
173
((string=? (car args) "videos")
174
(cli-videos config (cdr args)))
175
((string=? (car args) "analytics")
176
(cli-analytics config (cdr args) start-default end-default now-ts))
177
((string=? (car args) "stream-schedule")
178
(cli-schedule config (cdr args)))
179
((string=? (car args) "go-live")
180
(cli-go-live config (cdr args)))
181
((string=? (car args) "chat")
182
(cli-chat config (cdr args)))
183
((string=? (car args) "status")
184
(cli-status config (cdr args)))
185
(else
186
(display (string-append "Unknown command: " (car args) "\n\n"))
187
(print-usage)
188
(exit 1)))))
+201
(let* ((config (load-tube-config))
+202
(start-default (default-start-date))
+203
(end-default (default-end-date))
+204
(now-ts (default-now-timestamp))
+205
(cli (make-cli config start-default end-default now-ts)))
+206
(run-command cli (cdr (command-line)))))
207
208
))
src/tube/tools.sglmodified
@@ -127,17 +127,23 @@
127
(string-join (map format-analytics-row rows) "\n"))))))
128
(cons "twitch"
129
(lambda ()
130
(let* ((followers (tube-twitch-followers config 1))
131
(total (dict-ref followers total: 0))
132
(clips (tube-twitch-clips config 5)))
133
(string-append
134
"Twitch Analytics:\n"
135
"Total Followers: " (number->string total) "\n"
136
(if (null? clips)
137
"No recent clips."
138
(string-append
139
"Recent Clips:\n"
140
(string-join (map format-twitch-clip clips) "\n"))))))))
+130
(with-twitch-context config
+131
(lambda (client broadcaster-id)
+132
(let* ((followers (twitch-followers client broadcaster-id
+133
first: 1))
+134
(total (dict-ref followers total: 0))
+135
(clips (twitch-clips client
+136
broadcaster-id: broadcaster-id
+137
first: 5)))
+138
(string-append
+139
"Twitch Analytics:\n"
+140
"Total Followers: " (number->string total) "\n"
+141
(if (null? clips)
+142
"No recent clips."
+143
(string-append
+144
"Recent Clips:\n"
+145
(string-join (map format-twitch-clip clips)
+146
"\n"))))))))))
147
"No analytics data available. Check platform credentials.")))
148
149
(define (tool-schedule config params)
@@ -181,12 +187,11 @@
187
(youtube-broadcast-id broadcast)))))
188
(cons "twitch"
189
(lambda ()
184
(let ((client (require-twitch-client config))
185
(channel (tube-twitch-channel-info config)))
186
(twitch-modify-channel client
187
(twitch-channel-id channel)
188
#{ title: title })
189
(string-append "Twitch channel title set to: " title)))))
+190
(with-twitch-context config
+191
(lambda (client broadcaster-id)
+192
(twitch-modify-channel client broadcaster-id
+193
#{ title: title })
+194
(string-append "Twitch channel title set to: " title))))))
195
"Failed to start broadcast. Check platform credentials.")))
196
197
(define (tool-chat config params)
@@ -252,7 +257,7 @@
257
258
(mcp-server-register-tool! server
259
name: "tube/analytics"
255
description: "Get analytics summary combining YouTube and Twitch data. Shows views, watch time, subscribers/followers, and engagement."
+260
description: "Get analytics summary combining YouTube and Twitch data. YouTube provides day-by-day time-series (views, watch time, subs). Twitch shows aggregate follower counts and recent clips (the Twitch API does not expose queryable time-series analytics)."
261
schema: #{ type: "object"
262
properties: #{
263
days: #{ type: "integer"
@@ -290,7 +295,7 @@
295
296
(mcp-server-register-tool! server
297
name: "tube/chat"
293
description: "Send a message to live chat on the specified platform."
+298
description: "Send a message to live chat. Currently supports Twitch only. YouTube live chat requires an active broadcast and is not yet implemented."
299
schema: #{ type: "object"
300
properties: #{
301
message: #{ type: "string"
src/tube/twitch.sglmodified
@@ -16,6 +16,7 @@
16
(sigil twitch chat)
17
(tube config))
18
(export require-twitch-client
+19
with-twitch-context
20
tube-twitch-channel-info
21
tube-twitch-status
22
tube-twitch-schedule