Trying to unite wizard and selfcheck commands.
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Wed, 26 Mar 2025 01:20:43 +0000 (03:20 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Wed, 26 Mar 2025 01:20:43 +0000 (03:20 +0200)
src/main/java/eu/svjatoslav/alyverkko_cli/Main.java
src/main/java/eu/svjatoslav/alyverkko_cli/commands/JoinFilesCommand.java
src/main/java/eu/svjatoslav/alyverkko_cli/commands/ListModelsCommand.java
src/main/java/eu/svjatoslav/alyverkko_cli/commands/SelftestCommand.java [deleted file]
src/main/java/eu/svjatoslav/alyverkko_cli/commands/WizardCommand.java
src/main/java/eu/svjatoslav/alyverkko_cli/commands/mail_correspondant/MailCorrespondentCommand.java
src/main/java/eu/svjatoslav/alyverkko_cli/configuration/Configuration.java
src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationHelper.java [new file with mode: 0644]

index 25fa1fd..ed81cfb 100644 (file)
@@ -23,7 +23,6 @@ public class Main {
             new ListModelsCommand(),
             new MailCorrespondentCommand(),
             new JoinFilesCommand(),
-            new SelftestCommand(),
             new WizardCommand()
     );
 
index b72b440..1e644ed 100644 (file)
@@ -1,6 +1,7 @@
 package eu.svjatoslav.alyverkko_cli.commands;
 
 import eu.svjatoslav.alyverkko_cli.Command;
+import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper;
 import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser;
 import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.DirectoryOption;
 import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.NullOption;
@@ -15,7 +16,8 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.*;
 
 import static eu.svjatoslav.alyverkko_cli.Main.configuration;
-import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.loadConfiguration;
+import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.getConfigurationFile;
+import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.loadConfiguration;
 
 /**
  * The JoinFilesCommand aggregates multiple files (optionally matching
@@ -93,7 +95,7 @@ public class JoinFilesCommand implements Command {
      */
     @Override
     public void execute(String[] cliArguments) throws IOException {
-        configuration = loadConfiguration();
+        configuration = loadConfiguration(getConfigurationFile(null));
         if (configuration == null){
             System.out.println("Failed to load configuration file");
             return;
index 2b42712..56128a6 100644 (file)
@@ -6,7 +6,8 @@ import eu.svjatoslav.alyverkko_cli.model.ModelLibrary;
 import java.io.IOException;
 
 import static eu.svjatoslav.alyverkko_cli.Main.configuration;
-import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.loadConfiguration;
+import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.getConfigurationFile;
+import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.loadConfiguration;
 
 /**
  * Lists all configured models in the system, loading them from the
@@ -31,7 +32,7 @@ public class ListModelsCommand implements Command {
      */
     @Override
     public void execute(String[] cliArguments) throws IOException {
-        configuration = loadConfiguration();
+        configuration = loadConfiguration(getConfigurationFile(null));
         if (configuration == null){
             System.out.println("Failed to load configuration file");
             return;
diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/SelftestCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/SelftestCommand.java
deleted file mode 100644 (file)
index cfe7a2e..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-package eu.svjatoslav.alyverkko_cli.commands;
-
-import eu.svjatoslav.alyverkko_cli.AiTask;
-import eu.svjatoslav.alyverkko_cli.Command;
-import eu.svjatoslav.alyverkko_cli.Main;
-import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser;
-import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption;
-
-import java.io.File;
-import java.io.IOException;
-
-import static eu.svjatoslav.alyverkko_cli.Main.configuration;
-import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.loadConfiguration;
-
-/**
- * Performs a series of checks to ensure that the Älyverkko CLI is
- * configured correctly, including verifying directories, executables,
- * and model definitions.
- */
-public class SelftestCommand implements Command {
-
-    /**
-     * @return the name of this command, i.e., "selftest".
-     */
-    @Override
-    public String getName() {
-        return "selftest";
-    }
-
-    final Parser parser = new Parser();
-
-    /**
-     * Optional CLI argument for specifying a configuration file path.
-     */
-    public FileOption configFileOption = parser.add(new FileOption("Configuration file path"))
-            .addAliases("--config", "-c")
-            .mustExist();
-
-    /**
-     * Executes the self-test, checking if various directories, files, and
-     * configurations exist and are valid.
-     *
-     * @param cliArguments the command-line arguments following the "selftest" subcommand.
-     * @throws IOException if there are issues loading the configuration file.
-     */
-    @Override
-    public void execute(String[] cliArguments) throws IOException {
-        // Perform selftest checks here
-        System.out.println("Starting selftest...");
-
-        if (!parser.parse(cliArguments)) {
-            System.out.println("Failed to parse commandline arguments");
-            parser.showHelp();
-            return;
-        }
-
-        configuration = loadConfiguration(configFileOption.isPresent() ? configFileOption.getValue() : null);
-        if (configuration == null) {
-            System.out.println("Failed to load configuration file");
-            return;
-        }
-
-        boolean didAllChecksPass = true;
-
-        // Check if the configuration is loaded
-        if (Main.configuration == null) {
-            System.err.println("Configuration not found or invalid.");
-            didAllChecksPass = false;
-        } else {
-            // Validate models directory
-            if (!Main.configuration.getModelsDirectory().exists() || !Main.configuration.getModelsDirectory().isDirectory()) {
-                System.err.println("Models directory does not exist or is not a directory: " + Main.configuration.getModelsDirectory());
-                didAllChecksPass = false;
-            }
-
-            // Validate llama-cli executable path
-            if (!Main.configuration.getLlamaCliPath().exists() || !Main.configuration.getLlamaCliPath().isFile()) {
-                System.err.println("llama-cli executable path does not point to existing file: " + Main.configuration.getLlamaCliPath());
-                didAllChecksPass = false;
-            }
-
-            // Validate mail directory
-            if (!Main.configuration.getMailDirectory().exists() || !Main.configuration.getMailDirectory().isDirectory()) {
-                System.err.println("Mail directory does not exist or is not a directory: " + Main.configuration.getMailDirectory());
-                didAllChecksPass = false;
-            }
-
-            // Ensure that there is at least one prompt file
-            File promptsDirectory = Main.configuration.getPromptsDirectory();
-            if (promptsDirectory == null) {
-                System.err.println("Prompts directory is not defined in the configuration.");
-                didAllChecksPass = false;
-            } else {
-                if (!promptsDirectory.exists() || !promptsDirectory.isDirectory()) {
-                    System.err.println("Prompts directory does not exist or is not a directory: " + promptsDirectory);
-                    didAllChecksPass = false;
-                } else {
-                    if (promptsDirectory.listFiles() == null || promptsDirectory.listFiles().length == 0) {
-                        System.err.println("No prompt files found in the prompts directory: " + promptsDirectory);
-                        didAllChecksPass = false;
-                    }
-                }
-            }
-
-            // Validate models
-            if (Main.configuration.getModels().isEmpty()) {
-                System.err.println("No models are defined in the configuration.");
-                didAllChecksPass = false;
-            }
-
-            // Validate default temperature
-            if (Main.configuration.getDefaultTemperature() < 0 || Main.configuration.getDefaultTemperature() > 3) {
-                System.err.println("Default temperature must be between 0 and 3.");
-                didAllChecksPass = false;
-            }
-
-            // Validate thread count
-            if (Main.configuration.getThreadCount() < 1) {
-                System.err.println("Thread count must be at least 1.");
-                didAllChecksPass = false;
-            }
-
-            // Validate batch thread count
-            if (Main.configuration.getBatchThreadCount() < 1) {
-                System.err.println("Batch thread count must be at least 1.");
-                didAllChecksPass = false;
-            }
-
-            // Validate models again (redundant check but ensures clarity)
-            if (Main.configuration.getModels().isEmpty()) {
-                System.err.println("No models are defined in the configuration.");
-                didAllChecksPass = false;
-            }
-        }
-
-        if (didAllChecksPass) {
-            System.out.println("Selftest completed successfully.");
-        }
-    }
-}
index 00c3c74..3994ef7 100644 (file)
@@ -3,59 +3,62 @@ package eu.svjatoslav.alyverkko_cli.commands;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import eu.svjatoslav.alyverkko_cli.Command;
+import eu.svjatoslav.alyverkko_cli.Utils;
 import eu.svjatoslav.alyverkko_cli.configuration.Configuration;
 import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationModel;
+import eu.svjatoslav.commons.cli_helper.CLIHelper;
 import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser;
+import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption;
 import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.NullOption;
 import org.apache.commons.lang3.StringUtils;
 
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Scanner;
+import java.io.*;
+import java.nio.file.*;
+import java.util.*;
 import java.util.stream.Collectors;
 
-import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.DEFAULT_CONFIG_FILE_PATH;
-import static eu.svjatoslav.commons.cli_helper.CLIHelper.*;
+import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.getConfigurationFile;
+import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.loadConfiguration;
+import static eu.svjatoslav.commons.cli_helper.CLIHelper.askBoolean;
+import static eu.svjatoslav.commons.cli_helper.CLIHelper.askFloat;
+import static eu.svjatoslav.commons.cli_helper.CLIHelper.askInteger;
+import static java.lang.Boolean.TRUE;
 
 /**
- * The WizardCommand provides an interactive, console-based
- * configuration wizard for setting up the Älyverkko CLI application.
+ * A single WizardCommand that:
+ *   1. Loads existing configuration (if any).
+ *   2. Performs "selftest" style validation checks interactively,
+ *      prompting the user to fix invalid or missing items.
+ *   3. If no config file exists, it goes through all config parameters
+ *      from scratch.
+ *   4. Offers to remove model entries that reference missing files.
+ *   5. Autodiscovers new .gguf files and lets the user add them to the config.
+ *   6. Saves the resulting (fixed) config file.
+ *   7. Prints a final pass/fail summary for the user.
  */
 public class WizardCommand implements Command {
 
+    // Command-line parser to handle wizard arguments
     private final Parser parser = new Parser();
 
-    /**
-     * Command-line option to force overwriting an existing configuration file.
-     */
+    // If present, force overwriting existing config
     private final NullOption forceOverwriteOption = parser.add(new NullOption("Force overwrite existing configuration"))
             .addAliases("--force", "-f");
 
     /**
-     * An existing configuration, if one is found on disk.
+     * Optional CLI argument for specifying a configuration file path.
      */
-    private Configuration existingConfig;
+    public FileOption configFileOption = parser.add(new FileOption("Configuration file path"))
+            .addAliases("--config", "-c");
+
+    // The config object (loaded or newly created)
+    private Configuration config;
 
-    /**
-     * @return the name of this command, i.e., "wizard".
-     */
     @Override
     public String getName() {
         return "wizard";
     }
 
-    /**
-     * Executes this command, launching an interactive configuration
-     * wizard in the console.
-     *
-     * @param cliArguments the command-line arguments following the "wizard" subcommand.
-     * @throws IOException in case of IO failures while reading/writing config files.
-     */
     @Override
     public void execute(String[] cliArguments) throws IOException {
         if (!parser.parse(cliArguments)) {
@@ -64,163 +67,242 @@ public class WizardCommand implements Command {
             return;
         }
 
-        // Load existing configuration if it exists
-        existingConfig = Configuration.loadConfiguration(new File(DEFAULT_CONFIG_FILE_PATH));
-        if (existingConfig != null) {
-            System.out.println("Found existing configuration. Current values will be used as defaults.");
-        }
 
-        Configuration newConfig = new Configuration();
+        // 1. Load existing config if possible
+        File configFile = getConfigurationFile(configFileOption);
 
-        System.out.println("\nStarting Älyverkko CLI configuration wizard.\n");
-
-        // Mail Directory
-        String mailDirectory = getInputWithDefault(
-                "\nEnter the mail directory for AI tasks. This is where your task files will be stored.",
-                existingConfig != null ? existingConfig.getMailDirectory().getPath() : "~/.config/alyverkko-cli/mail"
-        );
-        newConfig.setMailDirectory(new File(mailDirectory));
-
-        // Models Directory
-        String modelsDirectory = getInputWithDefault(
-                "\nEnter the directory for AI models. This should contain your GGUF model files.",
-                existingConfig != null ? existingConfig.getModelsDirectory().getPath() : "~/.config/alyverkko-cli/models"
-        );
-        newConfig.setModelsDirectory(new File(modelsDirectory));
+        if (configFile.exists() && configFile.isFile()) {
+            System.out.println("Found existing configuration at " + configFile.getAbsolutePath());
+            System.out.println("I will check whether everything is valid, and if not, allow you to fix it.\n");
+            config = loadConfiguration(configFile);
+        } else {
+            // If no config found, create a fresh one
+            System.out.println("No existing configuration found. Let's create one!\n");
+            config = new Configuration();
+        }
 
-        // Prompts Directory
-        String promptsDirectory = getInputWithDefault(
-                "\nEnter the directory for prompts. This should contain text files with your AI prompts.",
-                existingConfig != null ? existingConfig.getPromptsDirectory().getPath() : "~/.config/alyverkko-cli/prompts"
-        );
-        newConfig.setPromptsDirectory(new File(promptsDirectory));
+        // 2. Validate and fix each parameter
+        checkAndFixAllParameters();
 
-        // Llama CLI Path
-        String llamaCliExecutablePath = getInputWithDefault(
-                "\nEnter the path to the llama-cli executable. This is the command-line interface for llama.cpp.",
-                existingConfig != null ? existingConfig.getLlamaCliPath().getPath() : "/usr/local/bin/llama-cli"
-        );
-        newConfig.setLlamaCliPath(new File(llamaCliExecutablePath));
+        // 3. Validate the models: remove or fix broken references, autodiscover new models, etc.
+        fixModelEntries();
 
-        // Default Temperature
-        float defaultTemperature = askFloat(
-                "\nEnter default temperature (0-2). A lower value makes the AI more deterministic, while a higher value makes it more creative.",
-                existingConfig != null ? existingConfig.getDefaultTemperature() : 0.7f,
-                0f, 2f
-        );
-        newConfig.setDefaultTemperature(defaultTemperature);
-
-        // Thread Counts
-        int threadCount = askInteger(
-                "\nEnter number of CPU threads for AI response generation. " +
-                        "RAM data transfer speed is usually the bottleneck here. " +
-                        "On a typical PC, while you might have 12 CPU cores, " +
-                        "RAM bandwidth can get already saturated with only 6 CPU threads. " +
-                        "Once RAM bandwidth is saturated, using more CPU threads " +
-                        "will not improve performance while it will keep CPU cores needlessly busy.",
-                existingConfig != null ? existingConfig.getThreadCount() : 6,
-                1, null
-        );
-        newConfig.setThreadCount(threadCount);
+        // 4. If the user is satisfied, attempt to save
+        trySaveConfiguration(configFile);
 
-        int batchThreadCount = askInteger(
-                "\nEnter number of CPU threads for input prompt processing. " +
-                        "CPU computing power is usually the bottleneck here (not the RAM bandwidth). " +
-                        "So you can utilize all CPU cores that you have here.",
-                existingConfig != null ? existingConfig.getBatchThreadCount() : 10,
-                1, null
-        );
-        newConfig.setBatchThreadCount(batchThreadCount);
+        // 5. Final selftest pass
+        boolean finalOk = doFinalCheck();
+        if (finalOk) {
+            System.out.println("\nConfiguration looks good. You're all set! You can now run:");
+            System.out.println("  alyverkko-cli mail   (to start the mail-based AI service)");
+            System.out.println("  alyverkko-cli joinfiles ...  (to prepare tasks for AI)\n");
+        } else {
+            System.out.println("\nSome checks did not pass, but we've saved the best config we could.");
+            System.out.println("You might want to run 'wizard' again or manually edit the YAML file:\n  "
+                    + configFile.getAbsolutePath() + "\n");
+        }
+    }
 
-        // Models Setup
-        System.out.println("\nModel configuration.");
-        List<ConfigurationModel> models = new ArrayList<>();
-        if (existingConfig != null) {
-            models.addAll(existingConfig.getModels());
+    /**
+     * Step-by-step checking (and possibly fixing) of each main config parameter.
+     */
+    private void checkAndFixAllParameters() {
+        // 2.1 mail_directory
+        while (true) {
+            File mailDir = config.getMailDirectory();
+            if (mailDir == null || !mailDir.isDirectory() || !mailDir.exists()) {
+                System.out.println("Mail directory is missing or invalid.");
+                String defaultVal = (mailDir == null) ? "~/.config/alyverkko-cli/mail" : mailDir.getPath();
+                String userInput = CLIHelper.askString(
+                        "Enter the mail directory path (will create if it doesn't exist)",
+                        defaultVal
+                );
+                mailDir = new File(userInput);
+                config.setMailDirectory(mailDir);
+
+                // Attempt to create if not exist
+                if (!mailDir.exists()) {
+                    boolean created = mailDir.mkdirs();
+                    if (!created) {
+                        Utils.printRedMessageToConsole("Failed to create mail directory. Check permissions?");
+                    }
+                }
+            } else {
+                // If valid, confirm or let user fix
+                if (askBoolean("Mail directory is: " + mailDir.getAbsolutePath() + " . Is this OK?", TRUE)) {
+                    break;
+                }
+                // Otherwise loop around to let them reenter
+                config.setMailDirectory(null); // Force re-check
+            }
         }
 
-        if (askBoolean("Would you like to try to autodiscover available undefined models ?", true)) {
-            discoverAndSuggestNewModels(newConfig.getModelsDirectory(), models);
+        // 2.2 models_directory
+        while (true) {
+            File modelsDir = config.getModelsDirectory();
+            if (modelsDir == null || !modelsDir.isDirectory() || !modelsDir.exists()) {
+                System.out.println("Models directory is missing or invalid.");
+                String defaultVal = (modelsDir == null) ? "~/.config/alyverkko-cli/models" : modelsDir.getPath();
+                String userInput = CLIHelper.askString(
+                        "Enter the models directory path (where your .gguf files are located)",
+                        defaultVal
+                );
+                modelsDir = new File(userInput);
+                config.setModelsDirectory(modelsDir);
+
+                // Attempt to create if not exist
+                if (!modelsDir.exists()) {
+                    boolean created = modelsDir.mkdirs();
+                    if (!created) {
+                        Utils.printRedMessageToConsole("Failed to create models directory. Check permissions?");
+                    }
+                }
+            } else {
+                if (askBoolean("Models directory is: " + modelsDir.getAbsolutePath() + " . Is this OK?", true)) {
+                    break;
+                }
+                config.setModelsDirectory(null);
+            }
         }
 
-        System.out.println("\nYou can now add models manually.");
+        // 2.3 prompts_directory
         while (true) {
-            String alias = askString("Enter desired model alias (leave empty to finish adding models): ");
-            if (StringUtils.isBlank(alias)) break;
+            File promptsDir = config.getPromptsDirectory();
+            if (promptsDir == null || !promptsDir.isDirectory() || !promptsDir.exists()) {
+                System.out.println("Prompts directory is missing or invalid.");
+                String defaultVal = (promptsDir == null) ? "~/.config/alyverkko-cli/prompts" : promptsDir.getPath();
+                String userInput = CLIHelper.askString(
+                        "Enter the prompts directory path",
+                        defaultVal
+                );
+                promptsDir = new File(userInput);
+                config.setPromptsDirectory(promptsDir);
+
+                // Attempt to create if not exist
+                if (!promptsDir.exists()) {
+                    boolean created = promptsDir.mkdirs();
+                    if (!created) {
+                        Utils.printRedMessageToConsole("Failed to create prompts directory. Check permissions?");
+                    }
+                }
+            } else {
+                if (askBoolean("Prompts directory is: " + promptsDir.getAbsolutePath() + " . Is this OK?", true)) {
+                    break;
+                }
+                config.setPromptsDirectory(null);
+            }
+        }
 
-            String filePath = getInputWithDefault(
-                    "\nEnter filesystem path relative to models directory. " +
-                            "Press Tab for suggestions or Enter to accept default.",
-                    null
-            );
+        // 2.4 llama_cli_path
+        while (true) {
+            File llamaCli = config.getLlamaCliPath();
+            boolean valid = (llamaCli != null && llamaCli.isFile() && llamaCli.exists());
+            if (!valid) {
+                System.out.println("The 'llama-cli' executable path is missing or invalid.");
+                String defaultVal = (llamaCli == null) ? "/usr/local/bin/llama-cli" : llamaCli.getPath();
+                String userInput = CLIHelper.askString(
+                        "Enter the path to the llama-cli executable",
+                        defaultVal
+                );
+                llamaCli = new File(userInput);
+                config.setLlamaCliPath(llamaCli);
+            } else {
+                if (askBoolean("Llama-cli path is: " + llamaCli.getAbsolutePath() + " . Is this OK?", true)) {
+                    break;
+                }
+                config.setLlamaCliPath(null);
+            }
+        }
 
-            int contextSize = askInteger(
-                    "\nEnter context size in tokens. A higher value allows the model to remember more context.",
-                    64000,
-                    1024,
-                    128000
-            );
+        // 2.5 default_temperature
+        float oldTemp = config.getDefaultTemperature();
+        if (oldTemp < 0.0f || oldTemp > 3.0f) {
+            oldTemp = 0.7f;
+        }
+        float newTemp = askFloat(
+                "\nEnter default temperature (0-3). Lower => more deterministic, higher => more creative.",
+                oldTemp,
+                0f, 3f
+        );
+        config.setDefaultTemperature(newTemp);
 
-            ConfigurationModel model = new ConfigurationModel();
-            model.setAlias(alias);
-            model.setFilesystemPath(filePath);
-            model.setContextSizeTokens(contextSize);
-            models.add(model);
+        // 2.6 thread_count
+        int oldThreadCount = config.getThreadCount();
+        if (oldThreadCount < 1) {
+            oldThreadCount = 6;
         }
+        int newThreadCount = askInteger(
+                "\nEnter number of CPU threads for AI generation. Typically 6 for a 12-core CPU, for example.",
+                oldThreadCount,
+                1, null
+        );
+        config.setThreadCount(newThreadCount);
 
-        newConfig.setModels(models);
+        // 2.7 batch_thread_count
+        int oldBatchThreadCount = config.getBatchThreadCount();
+        if (oldBatchThreadCount < 1) {
+            oldBatchThreadCount = 10;
+        }
+        int newBatchCount = askInteger(
+                "\nEnter number of CPU threads for input prompt processing (all cores is often fine).",
+                oldBatchThreadCount,
+                1, null
+        );
+        config.setBatchThreadCount(newBatchCount);
+    }
 
-        // Write configuration
-        Path configPath = Path.of(DEFAULT_CONFIG_FILE_PATH);
-        if (Files.exists(configPath) && !forceOverwriteOption.isPresent()) {
-            if (!askBoolean("Configuration file already exists. Overwrite?", false)) {
-                System.out.println("Configuration not saved. Run with --force to overwrite.");
-                return;
-            }
+    /**
+     * Offer to remove model references in config if the file does not exist.
+     * Then autodiscover new .gguf files and offer to add them.
+     * Then let user manually add more if desired.
+     */
+    private void fixModelEntries() {
+        if (config.getModels() == null) {
+            config.setModels(new ArrayList<>());
         }
 
-        // Create parent directories
-        Files.createDirectories(configPath.getParent());
+        // Remove references to nonexistent model files
+        removeDeadModelReferences();
 
-        // Save configuration
-        try (var writer = Files.newBufferedWriter(configPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
-            new ObjectMapper(new YAMLFactory()).writeValue(writer, newConfig);
+        // Offer to autodiscover new model files
+        if (askBoolean("\nAttempt to autodiscover new .gguf model files in the models directory?", true)) {
+            discoverAndSuggestNewModels(config.getModelsDirectory(), config.getModels());
         }
 
-        System.out.println("\nConfiguration saved to: " + configPath);
-        System.out.println("You can now run 'alyverkko-cli selftest' to verify everything is set up correctly.");
+        // Let user manually add new models
+        addModelsManually();
     }
 
     /**
-     * Prompts the user for an input value, showing a default value if provided.
-     *
-     * @param prompt       the prompt to display to the user.
-     * @param defaultValue the default value to use if the user simply presses Enter.
-     * @return the user's input, or the default if no input is provided.
+     * Loops through the config's list of models. If the file doesn't exist,
+     * let user decide whether to remove that model or keep it (perhaps to fix the path).
      */
-    private String getInputWithDefault(String prompt, String defaultValue) {
-        Scanner scanner = new Scanner(System.in);
-
-        if (defaultValue == null) {
-            System.out.print(prompt + ": ");
-        } else {
-            System.out.print(prompt + " [" + defaultValue + "]: ");
+    private void removeDeadModelReferences() {
+        Iterator<ConfigurationModel> it = config.getModels().iterator();
+        while (it.hasNext()) {
+            ConfigurationModel cm = it.next();
+            File f = new File(config.getModelsDirectory(), cm.getFilesystemPath());
+            if (!f.exists()) {
+                System.out.println("\nModel with alias '" + cm.getAlias()
+                        + "' references missing file: " + f.getAbsolutePath());
+                boolean remove = askBoolean("Remove this from config?", true);
+                if (remove) {
+                    it.remove();
+                } else {
+                    // Let them fix the filesystem_path or do so in next iteration
+                    System.out.println("Leaving it as-is for now. You can fix the path manually if needed.");
+                }
+            }
         }
-        String input = scanner.nextLine().trim();
-
-        return input.isEmpty() ? defaultValue : input;
     }
 
     /**
-     * Attempts to discover new .gguf model files in the given models directory, and
-     * suggests adding them to the configuration if they aren't already defined.
-     *
-     * @param modelsDir      the directory containing model files.
-     * @param existingModels existing model definitions to compare against.
+     * Scans the models directory for .gguf files not yet in the config,
+     * and offers to add them with user-chosen alias, etc.
      */
     private void discoverAndSuggestNewModels(File modelsDir, List<ConfigurationModel> existingModels) {
         if (!modelsDir.exists()) {
-            System.out.println("Models directory does not exist. Please create it first.");
+            System.out.println("Models directory does not exist. Please create it first or fix path above.");
             return;
         }
 
@@ -231,18 +313,30 @@ public class WizardCommand implements Command {
                     .filter(file -> file.getName().endsWith(".gguf") && !file.getName().contains("-of-"))
                     .collect(Collectors.toList());
 
-            List<String> existingAliases = existingModels.stream()
-                    .map(ConfigurationModel::getAlias)
+            List<String> existingPaths = existingModels.stream()
+                    .map(ConfigurationModel::getFilesystemPath)
                     .collect(Collectors.toList());
 
             for (File modelFile : modelFiles) {
                 String relativePath = modelsDir.toPath().relativize(modelFile.toPath()).toString();
-
-                boolean alreadyExists = existingModels.stream()
-                        .anyMatch(m -> m.getFilesystemPath().equals(relativePath));
-
-                if (!alreadyExists) {
-                    suggestAddingModel(existingModels, relativePath, existingAliases);
+                if (!existingPaths.contains(relativePath)) {
+                    System.out.println("\nDiscovered new model file: " + relativePath);
+                    String guessAlias = suggestAlias(relativePath);
+                    System.out.println("  Suggest alias: " + guessAlias);
+
+                    if (askBoolean("Would you like to add this model?", false)) {
+                        ConfigurationModel cm = new ConfigurationModel();
+                        cm.setAlias(CLIHelper.askString("Enter alias for this model", guessAlias));
+                        cm.setFilesystemPath(relativePath);
+                        int ctxSize = askInteger(
+                                "Enter context size in tokens (e.g. 64000).",
+                                64000, 1024, 128000
+                        );
+                        cm.setContextSizeTokens(ctxSize);
+                        cm.setEndOfTextMarker(null);
+                        existingModels.add(cm);
+                        existingPaths.add(relativePath);
+                    }
                 }
             }
         } catch (IOException e) {
@@ -251,41 +345,152 @@ public class WizardCommand implements Command {
     }
 
     /**
-     * Suggests to the user adding a newly-discovered model to the configuration.
-     *
-     * @param existingModels   the list of existing models to which we might add a new one.
-     * @param relativePath     the model file's path relative to the models directory.
-     * @param existingAliases  the aliases already defined for known models.
+     * Lets the user add one or more new models by specifying alias, path, context size, etc.
      */
-    private void suggestAddingModel(List<ConfigurationModel> existingModels, String relativePath, List<String> existingAliases) {
-        String alias = suggestAlias(relativePath);
-        System.out.println("\nFound new model: " + relativePath);
-        System.out.println("Suggested alias: " + alias);
-        if (askBoolean("Would you like to add this model? (y/N) ", false)) {
+    private void addModelsManually() {
+        System.out.println("\nNow you can add models manually, if you wish.");
+        while (true) {
+            String alias = CLIHelper.askString("Enter model alias (leave empty to finish adding models): ");
+            if (StringUtils.isBlank(alias)) {
+                break;
+            }
+            String filePath = CLIHelper.askString("Enter filesystem path relative to models directory: ");
+            if (StringUtils.isBlank(filePath)) {
+                System.out.println("Skipping because no path given.");
+                continue;
+            }
+            int contextSize = askInteger(
+                    "Enter context size in tokens (e.g. 64000).",
+                    64000, 1024, 128000
+            );
+
+            // Check for duplicates
+            boolean alreadyExists = config.getModels().stream()
+                    .anyMatch(m -> m.getAlias().equals(alias));
+            if (alreadyExists) {
+                Utils.printRedMessageToConsole("Model with alias '" + alias + "' already exists! Skipping.");
+                continue;
+            }
+
             ConfigurationModel model = new ConfigurationModel();
             model.setAlias(alias);
-            model.setFilesystemPath(relativePath);
-            int contextSize = askInteger("\nEnter context size in tokens. A higher value allows the model to remember more context.",
-                    64000,
-                    1024,
-                    null
-            );
+            model.setFilesystemPath(filePath);
             model.setContextSizeTokens(contextSize);
-            existingModels.add(model);
-            existingAliases.add(alias);
+            model.setEndOfTextMarker(null);
+
+            config.getModels().add(model);
         }
     }
 
     /**
-     * Generates a suggested model alias by removing non-alphanumeric characters,
-     * converting to lower case, and normalizing multiple dashes.
-     *
-     * @param filePath path string from which to suggest an alias.
-     * @return a suggested alias, derived from the model file name.
+     * Saves the config to the default path, verifying if user wants to
+     * overwrite if it already exists, etc.
+     */
+    private void trySaveConfiguration(File configFile) {
+        boolean alreadyExists = Files.exists(configFile.toPath());
+
+        if (alreadyExists && !forceOverwriteOption.isPresent()) {
+            boolean userOk = askBoolean("\nConfiguration file already exists. Overwrite it?", true);
+            if (!userOk) {
+                System.out.println("Not overwriting. If you want to do so, run again with --force.");
+                return;
+            }
+        }
+
+        try {
+            Files.createDirectories(configFile.toPath().getParent());
+            try (BufferedWriter writer = Files.newBufferedWriter(
+                    configFile.toPath(),
+                    StandardOpenOption.CREATE,
+                    StandardOpenOption.TRUNCATE_EXISTING
+            )) {
+                new ObjectMapper(new YAMLFactory()).writeValue(writer, config);
+            }
+            System.out.println("\nConfiguration saved to: " + configFile.toPath());
+        } catch (IOException e) {
+            Utils.printRedMessageToConsole("Error saving configuration: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Runs a final check across the config to confirm everything is
+     * valid. Returns true if all checks pass, false otherwise.
+     */
+    private boolean doFinalCheck() {
+        System.out.println("\nPerforming final checks...\n");
+        boolean allOk = true;
+
+        // Check mail dir
+        if (config.getMailDirectory() == null || !config.getMailDirectory().isDirectory()) {
+            System.err.println("Mail directory is invalid: " + config.getMailDirectory());
+            allOk = false;
+        }
+
+        // Check llama-cli
+        if (config.getLlamaCliPath() == null || !config.getLlamaCliPath().isFile()) {
+            System.err.println("llama-cli path is invalid: " + config.getLlamaCliPath());
+            allOk = false;
+        }
+
+        // Check prompts dir
+        if (config.getPromptsDirectory() == null || !config.getPromptsDirectory().isDirectory()) {
+            System.err.println("Prompts directory is invalid: " + config.getPromptsDirectory());
+            allOk = false;
+        } else {
+            File[] pFiles = config.getPromptsDirectory().listFiles();
+            if (pFiles == null || pFiles.length == 0) {
+                System.out.println("Warning: No prompt files found in " + config.getPromptsDirectory());
+                // not necessarily fatal, so we won't mark allOk = false
+            }
+        }
+
+        // Check models
+        if (config.getModels() == null || config.getModels().isEmpty()) {
+            System.err.println("No models are defined in the configuration.");
+            allOk = false;
+        } else {
+            // Check each model file
+            for (ConfigurationModel m : config.getModels()) {
+                File f = new File(config.getModelsDirectory(), m.getFilesystemPath());
+                if (!f.exists()) {
+                    System.err.println("Model alias '" + m.getAlias()
+                            + "' references missing file: " + f.getAbsolutePath());
+                    allOk = false;
+                }
+            }
+        }
+
+        // Check temperature
+        if (config.getDefaultTemperature() < 0 || config.getDefaultTemperature() > 3) {
+            System.err.println("Default temperature must be between 0 and 3. Found: " + config.getDefaultTemperature());
+            allOk = false;
+        }
+
+        if (config.getThreadCount() < 1) {
+            System.err.println("thread_count must be >= 1. Found: " + config.getThreadCount());
+            allOk = false;
+        }
+
+        if (config.getBatchThreadCount() < 1) {
+            System.err.println("batch_thread_count must be >= 1. Found: " + config.getBatchThreadCount());
+            allOk = false;
+        }
+
+        if (allOk) {
+            System.out.println("All final checks passed!\n");
+        } else {
+            System.out.println("\nSome checks failed (see messages above).");
+        }
+        return allOk;
+    }
+
+    /**
+     * Generates an alias from a .gguf filename by removing non-alphanumeric chars.
      */
     private String suggestAlias(String filePath) {
         String fileName = new File(filePath).getName();
         String alias = fileName.replaceAll("[^a-zA-Z0-9]", "-").toLowerCase();
         return alias.replaceAll("-+", "-").replaceAll("^-|-$", "");
     }
+
 }
index 7bb24ee..c83f2d4 100644 (file)
@@ -1,6 +1,7 @@
 package eu.svjatoslav.alyverkko_cli.commands.mail_correspondant;
 
 import eu.svjatoslav.alyverkko_cli.*;
+import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper;
 import eu.svjatoslav.alyverkko_cli.model.Model;
 import eu.svjatoslav.alyverkko_cli.model.ModelLibrary;
 import eu.svjatoslav.commons.cli_helper.parameter_parser.Parser;
@@ -16,7 +17,8 @@ import java.util.Map;
 import java.util.Optional;
 
 import static eu.svjatoslav.alyverkko_cli.Main.configuration;
-import static eu.svjatoslav.alyverkko_cli.configuration.Configuration.loadConfiguration;
+import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.getConfigurationFile;
+import static eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper.loadConfiguration;
 import static eu.svjatoslav.commons.file.IOHelper.getFileContentsAsString;
 import static eu.svjatoslav.commons.file.IOHelper.saveToFile;
 import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
@@ -27,7 +29,7 @@ import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
  * directory for new or modified text files, checks if they have a
  * "TOCOMPUTE:" marker, and if so, processes them with an AI model.
  * Once processed, results are appended to the same file.
- *
+ * <p>
  * Usage:
  * <pre>
  *   alyverkko-cli mail
@@ -88,7 +90,7 @@ public class MailCorrespondentCommand implements Command {
             return;
         }
 
-        configuration = loadConfiguration(configFileOption.isPresent() ? configFileOption.getValue() : null);
+        configuration = loadConfiguration(getConfigurationFile(configFileOption));
         if (configuration == null) {
             System.out.println("Failed to load configuration file");
             return;
index 3970e2f..dfa178b 100644 (file)
@@ -18,12 +18,6 @@ import static eu.svjatoslav.commons.file.IOHelper.getFileContentsAsString;
 @Data
 public class Configuration {
 
-    /**
-     * The default path for the YAML config file, typically under the user's home directory.
-     */
-    public static final String DEFAULT_CONFIG_FILE_PATH = "~/.config/alyverkko-cli/alyverkko-cli.yaml"
-            .replaceFirst("^~", System.getProperty("user.home"));
-
     /**
      * Directory where AI tasks (mail) are placed and discovered.
      */
@@ -74,38 +68,6 @@ public class Configuration {
      */
     private List<ConfigurationModel> models;
 
-    /**
-     * Loads the configuration from the default file path.
-     *
-     * @return the {@link Configuration} object, or null if the file doesn't exist or fails parsing.
-     * @throws IOException if file I/O fails during reading.
-     */
-    public static Configuration loadConfiguration() throws IOException {
-        return loadConfiguration(null);
-    }
-
-    /**
-     * Loads the configuration from a given file, or from the default
-     * path if {@code configFile} is null.
-     *
-     * @param configFile the file containing the YAML config; may be null.
-     * @return the {@link Configuration} object, or null if not found/invalid.
-     * @throws IOException if file I/O fails during reading.
-     */
-    public static Configuration loadConfiguration(File configFile) throws IOException {
-        if (configFile == null) {
-            // Load configuration from the default path
-            configFile = new File(DEFAULT_CONFIG_FILE_PATH);
-        }
-
-        if (!configFile.exists()) {
-            System.err.println("Configuration file not found: " + configFile);
-            return null;
-        }
-
-        ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
-        return mapper.readValue(configFile, Configuration.class);
-    }
 
     /**
      * Retrieves the contents of a prompt file by alias, e.g. "writer"
diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationHelper.java b/src/main/java/eu/svjatoslav/alyverkko_cli/configuration/ConfigurationHelper.java
new file mode 100644 (file)
index 0000000..45891db
--- /dev/null
@@ -0,0 +1,48 @@
+package eu.svjatoslav.alyverkko_cli.configuration;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import eu.svjatoslav.commons.cli_helper.parameter_parser.parameter.FileOption;
+
+import java.io.File;
+import java.io.IOException;
+
+public class ConfigurationHelper {
+
+    /**
+     * The default path for the YAML config file, typically under the user's home directory.
+     */
+    public static final String DEFAULT_CONFIG_FILE_PATH = "~/.config/alyverkko-cli/alyverkko-cli.yaml".replaceFirst("^~", System.getProperty("user.home"));
+
+    /**
+     * Loads the configuration from a given file, or from the default
+     * path if {@code configFile} is null.
+     *
+     * @param configFile the file containing the YAML config; may be null.
+     * @return the {@link Configuration} object, or null if not found/invalid.
+     * @throws IOException if file I/O fails during reading.
+     */
+    public static Configuration loadConfiguration(File configFile) throws IOException {
+
+        if (!configFile.exists()) {
+            System.err.println("Configuration file not found: " + configFile);
+            return null;
+        }
+
+        ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
+        return mapper.readValue(configFile, Configuration.class);
+    }
+
+    /**
+     * Returns the configuration file from the given option, or the default path if not present.
+     * @param configFileOption the CLI option for the config file.
+     * @return the configuration file to load.
+     */
+    public static File getConfigurationFile(FileOption configFileOption) {
+        if (configFileOption != null)
+            if (configFileOption.isPresent())
+                return configFileOption.getValue();
+
+        return new File(DEFAULT_CONFIG_FILE_PATH);
+    }
+}