Add `AddTaskHeaderCommand` to support adding TOCOMPUTE headers to files master
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sat, 17 Jan 2026 03:52:14 +0000 (05:52 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sat, 17 Jan 2026 03:52:14 +0000 (05:52 +0200)
src/main/java/eu/svjatoslav/alyverkko_cli/Main.java
src/main/java/eu/svjatoslav/alyverkko_cli/Utils.java
src/main/java/eu/svjatoslav/alyverkko_cli/commands/AddTaskHeaderCommand.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/alyverkko_cli/commands/package-info.java
src/main/java/eu/svjatoslav/alyverkko_cli/commands/task_processor/TaskProcessorCommand.java

index 289ede4..daea211 100644 (file)
@@ -12,7 +12,7 @@ import static java.util.Arrays.copyOfRange;
 /**
  * The main entry point for the Ã„lyverkko CLI application.
  * It processes subcommands such as "wizard", "selftest", "joinfiles",
- * "mail", and "listmodels".
+ * "process", etc...
  */
 public class Main {
 
@@ -23,7 +23,8 @@ public class Main {
             new ListModelsCommand(),
             new TaskProcessorCommand(),
             new JoinFilesCommand(),
-            new WizardCommand()
+            new WizardCommand(),
+            new AddTaskHeaderCommand()
     );
 
     /**
index 87e8531..c01c2a7 100644 (file)
@@ -1,5 +1,10 @@
 package eu.svjatoslav.alyverkko_cli;
 
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+
 /**
  * <p>General utility functions for the Ã„lyverkko CLI application. Currently provides ANSI color output capabilities for
  * console messages.
@@ -20,4 +25,31 @@ public class Utils {
         // reset output color
         System.out.print("\033[0m");
     }
+
+    /**
+     * Reads the first line of a file.
+     *
+     * @param file File to read
+     * @return First line of file or null if empty
+     * @throws IOException if file reading fails
+     */
+    public static String getFirstLine(File file) throws IOException {
+        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+            return reader.readLine();
+        }
+    }
+
+    /**
+     * Inspects the first line of the file to see if it starts with "TOCOMPUTE:".
+     *
+     * @param file the file to read.
+     * @return true if the file's first line starts with "TOCOMPUTE:".
+     * @throws IOException if file reading fails.
+     */
+    public static boolean fileHasToComputeMarker(File file) throws IOException {
+        String firstLine = getFirstLine(file);
+        return firstLine != null && firstLine.startsWith("TOCOMPUTE:");
+    }
+
+
 }
diff --git a/src/main/java/eu/svjatoslav/alyverkko_cli/commands/AddTaskHeaderCommand.java b/src/main/java/eu/svjatoslav/alyverkko_cli/commands/AddTaskHeaderCommand.java
new file mode 100644 (file)
index 0000000..d6dc0c7
--- /dev/null
@@ -0,0 +1,186 @@
+package eu.svjatoslav.alyverkko_cli.commands;
+import eu.svjatoslav.alyverkko_cli.Command;
+import eu.svjatoslav.alyverkko_cli.configuration.Configuration;
+import eu.svjatoslav.alyverkko_cli.configuration.ConfigurationHelper;
+import eu.svjatoslav.alyverkko_cli.configuration.Model;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.*;
+import java.util.*;
+
+import static eu.svjatoslav.alyverkko_cli.Utils.fileHasToComputeMarker;
+
+/**
+ * This command recursively adds a TOCOMPUTE header to all non-hidden files in the current directory
+ * that do not already have one. It prompts the user for skill, model, and priority values,
+ * validates their existence in the configuration, and then processes the files accordingly.
+ * <p>
+ * Usage:
+ * <pre>
+ *   alyverkko-cli addheader
+ * </pre>
+ * The command will interactively prompt for:
+ * - Skill name (must exist in skills directory)
+ * - Model alias (must exist in configuration models)
+ * - Priority value (integer, defaults to 0)
+ * <p>
+ * After validation, it will process all non-hidden files in the current directory and subdirectories,
+ * adding the TOCOMPUTE header at the beginning of files that don't already have one.
+ */
+public class AddTaskHeaderCommand implements Command {
+
+    @Override
+    public String getCommandName() {
+        return "addheader";
+    }
+
+    /**
+     * Executes the addheader command. Loads configuration, prompts user for skill, model, and priority,
+     * validates them, and processes all files in the current directory recursively.
+     *
+     * @param cliArguments command-line arguments (unused in this command)
+     * @throws IOException if file operations fail
+     * @throws InterruptedException if interrupted during processing
+     */
+    @Override
+    public void executeCommand(String[] cliArguments) throws IOException, InterruptedException {
+        // Load configuration to validate skills and models
+        Configuration config = ConfigurationHelper.loadConfiguration(ConfigurationHelper.getConfigurationFile(null));
+        if (config == null) {
+            System.err.println("ERROR: Failed to load configuration file");
+            return;
+        }
+
+        Scanner scanner = new Scanner(System.in);
+        String skill = promptForSkill(scanner, config);
+        String model = promptForModel(scanner, config);
+        int priority = promptForPriority(scanner);
+
+        System.out.println("\nProcessing files in current directory...");
+        processDirectory(new File("."), skill, model, priority);
+        System.out.println("\nProcessing complete!");
+    }
+
+    /**
+     * Prompts user for skill name and validates it exists in skills directory.
+     *
+     * @param scanner Scanner for user input
+     * @param config  Current configuration
+     * @return Validated skill name
+     */
+    private String promptForSkill(Scanner scanner, Configuration config) {
+        while (true) {
+            System.out.print("Enter skill name: ");
+            String skill = scanner.nextLine().trim();
+            File skillFile = new File(config.getSkillsDirectory(), skill + ".yaml");
+            if (skillFile.exists() && skillFile.isFile()) {
+                return skill;
+            }
+            System.out.println("ERROR: Skill '" + skill + "' not found in " + config.getSkillsDirectory());
+            System.out.println("Available skills: " + Arrays.toString(config.getSkillsDirectory().list((dir, name) -> name.endsWith(".yaml"))));
+        }
+    }
+
+    /**
+     * Prompts user for model alias and validates it exists in configuration.
+     *
+     * @param scanner Scanner for user input
+     * @param config  Current configuration
+     * @return Validated model alias
+     */
+    private String promptForModel(Scanner scanner, Configuration config) {
+        List<Model> models = config.getModels();
+        if (models == null || models.isEmpty()) {
+            System.err.println("ERROR: No models configured. Please check your configuration file.");
+            System.exit(1);
+        }
+
+        Map<String, Model> modelMap = new HashMap<>();
+        for (Model model : models) {
+            modelMap.put(model.getAlias(), model);
+        }
+
+        while (true) {
+            System.out.print("Enter model alias: ");
+            String alias = scanner.nextLine().trim();
+            if (modelMap.containsKey(alias)) {
+                return alias;
+            }
+            System.out.println("ERROR: Model '" + alias + "' not found. Available models: " + String.join(", ", modelMap.keySet()));
+        }
+    }
+
+    /**
+     * Prompts user for priority value with validation.
+     *
+     * @param scanner Scanner for user input
+     * @return Validated priority integer
+     */
+    private int promptForPriority(Scanner scanner) {
+        while (true) {
+            System.out.print("Enter priority (integer, default 0): ");
+            String input = scanner.nextLine().trim();
+            if (input.isEmpty()) {
+                return 0;
+            }
+            try {
+                int priority = Integer.parseInt(input);
+                if (priority < 0) {
+                    System.out.println("Priority must be non-negative. Using 0 as default.");
+                    return 0;
+                }
+                return priority;
+            } catch (NumberFormatException e) {
+                System.out.println("ERROR: Invalid priority. Please enter an integer.");
+            }
+        }
+    }
+
+    /**
+     * Recursively processes all files in a directory, skipping hidden files.
+     *
+     * @param dir     Directory to process
+     * @param skill   Skill name to include in TOCOMPUTE header
+     * @param model   Model alias to include in TOCOMPUTE header
+     * @param priority Priority value to include in TOCOMPUTE header
+     * @throws IOException if file operations fail
+     */
+    private void processDirectory(File dir, String skill, String model, int priority) throws IOException {
+        File[] files = dir.listFiles();
+        if (files == null) return;
+
+        for (File file : files) {
+            if (file.isDirectory()) {
+                processDirectory(file, skill, model, priority);
+            } else if (file.isFile() && !file.getName().startsWith(".")) {
+                processFile(file, skill, model, priority);
+            }
+        }
+    }
+
+    /**
+     * Processes a single file by adding TOCOMPUTE header if not already present.
+     *
+     * @param file    File to process
+     * @param skill   Skill name for TOCOMPUTE header
+     * @param model   Model alias for TOCOMPUTE header
+     * @param priority Priority value for TOCOMPUTE header
+     * @throws IOException if file operations fail
+     */
+    private void processFile(File file, String skill, String model, int priority) throws IOException {
+
+        if (fileHasToComputeMarker(file)) {
+            System.out.println("Skipped (already has header): " + file.getAbsolutePath());
+            return;
+        }
+
+        String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
+        String newContent = String.format("TOCOMPUTE: skill=%s model=%s priority=%d\n%s", skill, model, priority, content);
+
+        Files.write(file.toPath(), newContent.getBytes(StandardCharsets.UTF_8));
+        System.out.println("Added header: " + file.getAbsolutePath());
+    }
+
+
+}
index 871a60b..906a20e 100644 (file)
@@ -7,6 +7,7 @@
  *   <li>Model listing and management</li>
  *   <li>File joining for multi-file processing</li>
  *   <li>Mail-based AI task processing</li>
+ *   <li>Adding TOCOMPUTE headers to files</li>
  * </ul>
  */
 
index c811509..7ac02c2 100644 (file)
@@ -16,6 +16,8 @@ import java.nio.file.*;
 import java.util.*;
 
 import static eu.svjatoslav.alyverkko_cli.Main.configuration;
+import static eu.svjatoslav.alyverkko_cli.Utils.fileHasToComputeMarker;
+import static eu.svjatoslav.alyverkko_cli.Utils.getFirstLine;
 import static eu.svjatoslav.commons.file.IOHelper.getFileContentsAsString;
 import static eu.svjatoslav.commons.file.IOHelper.saveToFile;
 import static java.nio.file.StandardWatchEventKinds.*;
@@ -181,24 +183,6 @@ public class TaskProcessorCommand implements Command {
         return fileHasToComputeMarker(file);
     }
 
-    /**
-     * Inspects the first line of the file to see if it starts with "TOCOMPUTE:".
-     *
-     * @param file the file to read.
-     * @return true if the file's first line starts with "TOCOMPUTE:".
-     * @throws IOException if file reading fails.
-     */
-    private static boolean fileHasToComputeMarker(File file) throws IOException {
-        String firstLine = getFirstLine(file);
-        return firstLine != null && firstLine.startsWith("TOCOMPUTE:");
-    }
-
-    private static String getFirstLine(File file) throws IOException {
-        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
-            return reader.readLine();
-        }
-    }
-
     /**
      * Persists AI response to the file; deletes the original file
      */