@@ -68,6 +68,9 @@ func runWorkspaceQuery(mode, workspace string, args []string) error {
6868 if mode == mcptools .FileQueryModeHybrid && isWorkspaceQueryIndexInvocation (args ) {
6969 return runWorkspaceQueryIndex (workspace , args [1 :])
7070 }
71+ if mode == mcptools .FileQueryModeHybrid && workspace == "" && isWorkspaceQueryModelInvocation (args ) {
72+ return runWorkspaceQueryModel (args [1 :])
73+ }
7174 opts , err := parseWorkspaceQueryArgs (mode , args )
7275 if err != nil {
7376 return err
@@ -84,6 +87,121 @@ func runWorkspaceQuery(mode, workspace string, args []string) error {
8487 return runWorkspaceQueryRequest (ctx , remote , opts , request )
8588}
8689
90+ func isWorkspaceQueryModelInvocation (args []string ) bool {
91+ if len (args ) == 0 || args [0 ] != "model" {
92+ return false
93+ }
94+ if len (args ) == 1 || isHelpArg (args [1 ]) {
95+ return true
96+ }
97+ switch strings .TrimSpace (args [1 ]) {
98+ case "status" , "download" :
99+ return true
100+ default :
101+ return false
102+ }
103+ }
104+
105+ func runWorkspaceQueryModel (args []string ) error {
106+ if len (args ) == 0 || isHelpArg (args [0 ]) {
107+ fmt .Fprint (os .Stderr , workspaceQueryModelUsageText (filepath .Base (os .Args [0 ])))
108+ return nil
109+ }
110+ switch strings .TrimSpace (args [0 ]) {
111+ case "status" :
112+ return runWorkspaceQueryModelStatus (args [1 :])
113+ case "download" :
114+ return runWorkspaceQueryModelDownload (args [1 :])
115+ default :
116+ return fmt .Errorf ("unknown query model subcommand %q\n \n %s" , args [0 ], workspaceQueryModelUsageText (filepath .Base (os .Args [0 ])))
117+ }
118+ }
119+
120+ func runWorkspaceQueryModelStatus (args []string ) error {
121+ fs := flag .NewFlagSet ("query model status" , flag .ContinueOnError )
122+ fs .SetOutput (io .Discard )
123+ var jsonOut bool
124+ var model string
125+ fs .BoolVar (& jsonOut , "json" , false , "write JSON output" )
126+ fs .StringVar (& model , "model" , "" , "local model id" )
127+ if err := fs .Parse (args ); err != nil || fs .NArg () != 0 {
128+ return fmt .Errorf ("%s" , workspaceQueryModelUsageText (filepath .Base (os .Args [0 ])))
129+ }
130+ ctx := context .Background ()
131+ _ , service , closeFn , err := openAFSControlPlane (ctx )
132+ if err != nil {
133+ return err
134+ }
135+ defer closeFn ()
136+ status , err := service .QueryModelStatus (ctx , controlplane.QueryModelStatusRequest {Model : model })
137+ if err != nil {
138+ return workspaceQueryModelControlPlaneError (err )
139+ }
140+ if jsonOut {
141+ enc := json .NewEncoder (os .Stdout )
142+ enc .SetIndent ("" , " " )
143+ return enc .Encode (status )
144+ }
145+ fmt .Fprintln (os .Stdout , "Query model" )
146+ fmt .Fprintln (os .Stdout )
147+ fmt .Fprintf (os .Stdout , "model %s\n " , status .Spec .ID )
148+ fmt .Fprintf (os .Stdout , "cache_dir %s\n " , status .CacheDir )
149+ fmt .Fprintf (os .Stdout , "path %s\n " , status .Path )
150+ fmt .Fprintf (os .Stdout , "downloaded %t\n " , status .Exists )
151+ if status .SizeBytes > 0 {
152+ fmt .Fprintf (os .Stdout , "size %s\n " , formatBytes (status .SizeBytes ))
153+ }
154+ return nil
155+ }
156+
157+ func runWorkspaceQueryModelDownload (args []string ) error {
158+ fs := flag .NewFlagSet ("query model download" , flag .ContinueOnError )
159+ fs .SetOutput (io .Discard )
160+ var jsonOut bool
161+ var model string
162+ fs .BoolVar (& jsonOut , "json" , false , "write JSON output" )
163+ fs .StringVar (& model , "model" , "" , "local model id" )
164+ if err := fs .Parse (args ); err != nil || fs .NArg () != 0 {
165+ return fmt .Errorf ("%s" , workspaceQueryModelUsageText (filepath .Base (os .Args [0 ])))
166+ }
167+ ctx := context .Background ()
168+ _ , service , closeFn , err := openAFSControlPlane (ctx )
169+ if err != nil {
170+ return err
171+ }
172+ defer closeFn ()
173+ if ! jsonOut {
174+ fmt .Fprintln (os .Stderr , "Resolving local embedding model on the control plane..." )
175+ }
176+ result , err := service .DownloadQueryModel (ctx , controlplane.QueryModelDownloadRequest {Model : model })
177+ if err != nil {
178+ return workspaceQueryModelControlPlaneError (err )
179+ }
180+ if jsonOut {
181+ enc := json .NewEncoder (os .Stdout )
182+ enc .SetIndent ("" , " " )
183+ return enc .Encode (result )
184+ }
185+ fmt .Fprintln (os .Stdout , "Query model" )
186+ fmt .Fprintln (os .Stdout )
187+ fmt .Fprintf (os .Stdout , "model %s\n " , result .Spec .ID )
188+ fmt .Fprintf (os .Stdout , "cache_dir %s\n " , result .CacheDir )
189+ fmt .Fprintf (os .Stdout , "path %s\n " , result .Path )
190+ fmt .Fprintf (os .Stdout , "cached %t\n " , result .Exists )
191+ fmt .Fprintf (os .Stdout , "resolved %t\n " , result .Downloaded || result .Exists )
192+ if result .SizeBytes > 0 {
193+ fmt .Fprintf (os .Stdout , "size %s\n " , formatBytes (result .SizeBytes ))
194+ }
195+ return nil
196+ }
197+
198+ func workspaceQueryModelControlPlaneError (err error ) error {
199+ if errors .Is (err , os .ErrNotExist ) {
200+ return fmt .Errorf ("query model routes are not available on this control plane; rebuild and restart afs-control-plane" )
201+ }
202+ return err
203+ }
204+
87205func isWorkspaceQueryIndexInvocation (args []string ) bool {
88206 if len (args ) == 0 || args [0 ] != "index" {
89207 return false
@@ -726,16 +844,18 @@ func workspaceQueryUsageText(bin, mode string) string {
726844 return brandHeaderString () + fmt .Sprintf (`Usage:
727845 %s query [flags] <query>
728846 %s fs [workspace] query [flags] <query>
729- %s query index <status|rebuild|clean> [flags]
847+ %s query index <status|create|rebuild|clean> [flags]
848+ %s query model <status|download> [flags]
730849
731850QMD-style hybrid + rerank workspace query.
732851Plain text runs hybrid retrieval by default. Use --keyword for keyword-ranked
733852retrieval only, or --semantic for vector-only semantic search.
734853
735854Default query currently falls back to keyword ranked results until hybrid
736855vector/rerank is complete. Use --semantic for vector-only retrieval. Semantic
737- embeddings are globally enabled and use OpenAI when OPENAI_API_KEY is set in
738- the control-plane environment.
856+ embeddings are globally enabled and default to OpenAI when OPENAI_API_KEY is set
857+ in the control-plane environment. Set AFS_EMBED_PROVIDER=local to use the local
858+ GGUF helper.
739859Use grep when you know the exact text.
740860
741861Typed query documents:
@@ -770,6 +890,33 @@ Examples:
770890 %s query --keyword "checkpoint savepoint"
771891 %s query --semantic "how do I save a snapshot?"
772892 %s query index status
893+ %s query model download
773894 %s fs repo query $'lex: checkpoint\nvec: how do I save a snapshot?'
774- ` , bin , bin , bin , bin , bin , bin , bin , bin )
895+ ` , bin , bin , bin , bin , bin , bin , bin , bin , bin , bin )
896+ }
897+
898+ func workspaceQueryModelUsageText (bin string ) string {
899+ return brandHeaderString () + fmt .Sprintf (`Usage:
900+ %[1]s query model <status|download> [flags]
901+
902+ Manage the control-plane global local GGUF embedding model cache.
903+
904+ Subcommands:
905+ status Show the configured local model and expected cache path
906+ download Ask the Node helper to resolve/download/load the model now
907+
908+ Flags:
909+ --model <model> Model id, default hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf
910+ --json Write JSON output
911+
912+ Environment:
913+ AFS_EMBED_MODEL_DIR Control-plane cache directory override
914+ AFS_EMBED_HELPER_CMD Control-plane Node.js command override
915+ AFS_NODE_LLAMA_CPP_MODULE
916+ node-llama-cpp module specifier override
917+
918+ Examples:
919+ %[1]s query model status
920+ %[1]s query model download
921+ ` , bin )
775922}
0 commit comments