@@ -21,7 +21,14 @@ struct PerspectiveCLI {
2121 // colocated metallib search succeeds regardless of install location.
2222 Self . chdirToExecutableDirectory ( )
2323
24- await CLIApp . shared. run ( )
24+ let args = CLIArguments ( )
25+
26+ if args. help {
27+ printUsage ( )
28+ return
29+ }
30+
31+ await CLIApp . shared. run ( with: args)
2532 }
2633
2734 /// Change working directory to the real (symlink-resolved) executable directory.
@@ -49,6 +56,50 @@ enum CLIError: LocalizedError {
4956 }
5057}
5158
59+ // MARK: - CLI Arguments
60+
61+ struct CLIArguments {
62+ var backend : Backend ?
63+ var mlxModel : String ?
64+ var prompt : String ?
65+ var temperature : Float ?
66+ var stream : Bool = false
67+ var systemPrompt : String ?
68+ var tools : Bool = false
69+ var help : Bool = false
70+
71+ init ( ) {
72+ let args = CommandLine . arguments. dropFirst ( ) // skip executable path
73+ var iter = args. makeIterator ( )
74+ while let arg = iter. next ( ) {
75+ switch arg {
76+ case " --fm " :
77+ backend = . fm
78+ case " --mlx " :
79+ backend = . mlx
80+ case " --mlx-model " :
81+ mlxModel = iter. next ( )
82+ case " --prompt " :
83+ prompt = iter. next ( )
84+ case " --temperature " :
85+ if let val = iter. next ( ) { temperature = Float ( val) }
86+ case " --stream " :
87+ stream = true
88+ case " --system " :
89+ systemPrompt = iter. next ( )
90+ case " --tools " :
91+ tools = true
92+ case " --help " , " -h " :
93+ help = true
94+ default :
95+ printError ( " Unknown argument: \( arg) " )
96+ help = true
97+ return
98+ }
99+ }
100+ }
101+ }
102+
52103// MARK: - Backend Selection
53104
54105enum Backend : String {
@@ -71,6 +122,106 @@ actor CLIApp {
71122
72123 private init ( ) { }
73124
125+ /// Apply CLI arguments to configure backends before starting.
126+ private func applyArgs( _ args: CLIArguments ) async {
127+ if let backend = args. backend {
128+ activeBackend = backend
129+ }
130+ if let system = args. systemPrompt {
131+ customSystemPrompt = system
132+ }
133+ if args. tools {
134+ toolsEnabled = true
135+ }
136+ if args. stream {
137+ await fmBackend. setStreaming ( true )
138+ }
139+ if let temp = args. temperature {
140+ switch activeBackend {
141+ case . fm:
142+ await fmBackend. setTemperature ( temp)
143+ case . mlx:
144+ await mlxBackend. setTemperature ( temp)
145+ }
146+ }
147+ if let model = args. mlxModel {
148+ await mlxBackend. setModelId ( model)
149+ }
150+ }
151+
152+ /// Initialize the active backend, returning true on success.
153+ private func initializeActiveBackend( quiet: Bool = false ) async -> Bool {
154+ switch activeBackend {
155+ case . fm:
156+ let ( available, msg) = FoundationModelsBackend . checkAvailability ( )
157+ if !available {
158+ printError ( msg)
159+ return false
160+ }
161+ if !quiet { printSuccess ( msg) }
162+ await fmBackend. initialize ( customPrompt: customSystemPrompt, enableTools: toolsEnabled)
163+ if !quiet { printSuccess ( " FM session initialized " ) }
164+ return true
165+ case . mlx:
166+ do {
167+ try await mlxBackend. initialize ( customPrompt: customSystemPrompt)
168+ if !quiet { printSuccess ( " MLX session initialized " ) }
169+ return true
170+ } catch {
171+ printError ( " Failed to initialize MLX: \( error. localizedDescription) " )
172+ return false
173+ }
174+ }
175+ }
176+
177+ /// One-shot mode: send a prompt, print the response, and exit.
178+ private func runOneShot( _ prompt: String ) async {
179+ guard await initializeActiveBackend ( quiet: true ) else { return }
180+
181+ do {
182+ switch activeBackend {
183+ case . fm:
184+ if await fmBackend. isStreaming ( ) {
185+ let stream = try await fmBackend. streamMessage ( prompt)
186+ var lastLength = 0
187+ for try await partial in stream {
188+ let newContent = String ( partial. dropFirst ( lastLength) )
189+ print ( newContent, terminator: " " )
190+ fflush ( stdout)
191+ lastLength = partial. count
192+ }
193+ print ( " " )
194+ } else {
195+ let response = try await fmBackend. sendMessage ( prompt)
196+ print ( response)
197+ }
198+ case . mlx:
199+ let stream = try await mlxBackend. streamMessage ( prompt)
200+ var lastLength = 0
201+ for try await partial in stream {
202+ let newContent = String ( partial. dropFirst ( lastLength) )
203+ print ( newContent, terminator: " " )
204+ fflush ( stdout)
205+ lastLength = partial. count
206+ }
207+ print ( " " )
208+ }
209+ } catch {
210+ printError ( " Error: \( error. localizedDescription) " )
211+ }
212+ }
213+
214+ func run( with args: CLIArguments ) async {
215+ await applyArgs ( args)
216+
217+ if let prompt = args. prompt {
218+ await runOneShot ( prompt)
219+ return
220+ }
221+
222+ await run ( )
223+ }
224+
74225 func run( ) async {
75226 printWelcome ( )
76227
@@ -85,11 +236,21 @@ actor CLIApp {
85236 }
86237
87238 // Initialize FM backend if available
88- if fmAvailable {
239+ if fmAvailable && activeBackend == . fm {
89240 await fmBackend. initialize ( customPrompt: customSystemPrompt, enableTools: toolsEnabled)
90241 printSuccess ( " FM session initialized " )
91242 }
92243
244+ // Initialize MLX backend if selected via args
245+ if activeBackend == . mlx {
246+ do {
247+ try await mlxBackend. initialize ( customPrompt: customSystemPrompt)
248+ printSuccess ( " MLX session initialized " )
249+ } catch {
250+ printError ( " Failed to initialize MLX: \( error. localizedDescription) " )
251+ }
252+ }
253+
93254 printHelp ( )
94255 print ( " " )
95256
@@ -414,3 +575,26 @@ func printWarning(_ message: String) {
414575func printInfo( _ message: String ) {
415576 print ( " \u{001B} [90m \( message) \u{001B} [0m " )
416577}
578+
579+ func printUsage( ) {
580+ print ( " Usage: perspective [options] " )
581+ print ( " " )
582+ print ( " Options: " )
583+ print ( " --fm Use Foundation Models backend " )
584+ print ( " --mlx Use MLX backend " )
585+ print ( " --mlx-model <id> Set MLX model (e.g. mlx-community/gemma-3-4b-it-4bit) " )
586+ print ( " --prompt <text> Send a prompt and exit (one-shot mode) " )
587+ print ( " --temperature <float> Set temperature (FM: 0.0-1.0, MLX: 0.0-2.0) " )
588+ print ( " --stream Enable streaming output (FM) " )
589+ print ( " --system <text> Set a custom system prompt " )
590+ print ( " --tools Enable tool calling (FM) " )
591+ print ( " --help, -h Show this help " )
592+ print ( " " )
593+ print ( " Examples: " )
594+ print ( " perspective --fm --prompt \" What is Swift? \" " )
595+ print ( " perspective --mlx --prompt \" Hello \" " )
596+ print ( " perspective --mlx --mlx-model mlx-community/gemma-3-4b-it-4bit " )
597+ print ( " perspective --temperature 0.5 --prompt \" Be creative \" " )
598+ print ( " " )
599+ print ( " Without --prompt, enters interactive REPL mode. " )
600+ }
0 commit comments