← Back to task

Commit 7e249eaa

commit 7e249eaa0f17e9af5d174665c156f09800957589
Author: Ben Sima <ben@bensima.com>
Date:   Thu Jan 1 18:33:16 2026

    Omni/Agent: Support shebang execution of markdown files
    
    When the prompt argument is a path to a .md/.markdown file,
    read the file content as the prompt instead of using the path literally.
    
    This enables executable markdown skills:
    
      #!/usr/bin/env agent
    
      Do something useful.
    
    Then run:
      chmod +x skill.md
      ./skill.md
    
    The shebang line is automatically stripped from the content.
    
    Task-Id: t-334

diff --git a/Omni/Agent.hs b/Omni/Agent.hs
index 228858ac..573585e8 100644
--- a/Omni/Agent.hs
+++ b/Omni/Agent.hs
@@ -39,8 +39,8 @@ import qualified Omni.Agent.Provider as Provider
 import qualified Omni.Agent.Tools as Tools
 import qualified Omni.Cli as Cli
 import qualified Omni.Test as Test
-import System.Directory (doesFileExist)
 import qualified System.Console.Docopt as Docopt
+import System.Directory (doesFileExist)
 import qualified System.Environment as Environment
 import qualified System.Exit as Exit
 import qualified System.IO as IO
@@ -115,7 +115,7 @@ run :: Docopt.Arguments -> IO ()
 run args = do
   -- Parse options
   opts <- parseOptions args
-  
+
   -- Check for dry-run
   when (optDryRun opts) <| do
     TextIO.hPutStrLn IO.stderr <| "Dry run - would use options:"
@@ -126,10 +126,10 @@ run args = do
     TextIO.hPutStrLn IO.stderr <| "  Max iter: " <> tshow (optMaxIter opts)
     TextIO.hPutStrLn IO.stderr <| "  Prompt: " <> Text.take 100 (optPrompt opts)
     Exit.exitSuccess
-  
+
   -- Run agent
   result <- runAgent opts
-  
+
   -- Output result
   if optJson opts
     then BL.putStr <| AesonPretty.encodePretty result
@@ -185,11 +185,30 @@ instance Aeson.ToJSON AgentResult where
         "error" Aeson..= err
       ]
 
+-- | Strip shebang line from file content (for executable markdown support)
+stripShebang :: Text -> Text
+stripShebang content =
+  case Text.lines content of
+    (line : rest) | "#!" `Text.isPrefixOf` line -> Text.unlines rest
+    _ -> content
+
 -- | Parse CLI options from docopt arguments
 parseOptions :: Docopt.Arguments -> IO AgentOptions
 parseOptions args = do
-  -- Get prompt from arg or stdin (docopt returns String, convert to Text)
-  let argPrompt = maybe "" Text.pack <| Docopt.getArg args (Docopt.argument "prompt")
+  -- Get prompt from arg or stdin
+  -- If arg is a file path (e.g., shebang invocation), read file content
+  argPrompt <- case Docopt.getArg args (Docopt.argument "prompt") of
+    Nothing -> pure ""
+    Just arg -> do
+      -- Check if it's a file path (for shebang support: ./script.md)
+      isFile <- doesFileExist arg
+      if isFile && (".md" `isSuffixOf` arg || ".markdown" `isSuffixOf` arg)
+        then do
+          content <- TextIO.readFile arg
+          -- Strip shebang line if present
+          pure <| stripShebang content
+        else pure (Text.pack arg)
+
   -- Always read stdin if not a terminal (allows piping data to prompts)
   stdinContent <- do
     isTerminal <- IO.hIsTerminalDevice IO.stdin
@@ -197,12 +216,10 @@ parseOptions args = do
       then pure ""
       else TextIO.getContents
   -- Combine: if we have stdin, append it to prompt; if no prompt, use stdin
-  let prompt = 
-        if Text.null stdinContent
-          then argPrompt
-          else if Text.null argPrompt
-            then stdinContent
-            else argPrompt <> "\n\n--- Input Data ---\n" <> stdinContent
+  let prompt
+        | Text.null stdinContent = argPrompt
+        | Text.null argPrompt = stdinContent
+        | otherwise = argPrompt <> "\n\n--- Input Data ---\n" <> stdinContent
 
   pure
     AgentOptions
@@ -301,7 +318,7 @@ resolveAutoProvider mModel = do
   -- Try providers in order of preference
   anthropicKey <- Environment.lookupEnv "ANTHROPIC_API_KEY"
   openrouterKey <- Environment.lookupEnv "OPENROUTER_API_KEY"
-  
+
   case (anthropicKey, openrouterKey) of
     (Just key, _) -> do
       TextIO.hPutStrLn IO.stderr "[agent] Using Anthropic provider"