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