From 375cf189a94dd9c191ed17c066a8cf0c56bd3e7c Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Fri, 28 Nov 2025 01:50:40 -0500 Subject: Implement jr facts list/show/add/delete CLI commands The build and tests pass. The hlint suggestions have been fixed: 1. Changed `Text.pack <$>` to `Text.pack ` to `Text.pack ] [--auto] jr loop [--delay=SECONDS] + jr facts list [--project=PROJECT] [--json] + jr facts show [--json] + jr facts add [--files=FILES] [--task=TASK] [--confidence=CONF] [--json] + jr facts delete [--json] jr test jr (-h | --help) @@ -61,12 +68,18 @@ Commands: web Start the web UI server review Review a completed task (show diff, accept/reject) loop Run autonomous work+review loop + facts Manage knowledge base facts Options: - -h --help Show this help - --port=PORT Port for web server [default: 8080] - --auto Auto-review: accept if tests pass, reject if they fail - --delay=SECONDS Delay between loop iterations [default: 5] + -h --help Show this help + --port=PORT Port for web server [default: 8080] + --auto Auto-review: accept if tests pass, reject if they fail + --delay=SECONDS Delay between loop iterations [default: 5] + --project=PROJECT Filter facts by project + --files=FILES Comma-separated list of related files + --task=TASK Source task ID + --confidence=CONF Confidence level 0.0-1.0 [default: 0.8] + --json Output in JSON format |] move :: Cli.Arguments -> IO () @@ -115,6 +128,7 @@ move args Just d -> fromMaybe 5 (readMaybe d) Nothing -> 5 runLoop delay + | args `Cli.has` Cli.command "facts" = handleFacts args | otherwise = putText (str <| Docopt.usage help) -- | Run the autonomous loop: work -> review -> repeat @@ -507,6 +521,79 @@ checkEpicCompletion task = where hasParent pid t = maybe False (TaskCore.matchesId pid) (TaskCore.taskParent t) +-- | Handle facts subcommands +handleFacts :: Cli.Arguments -> IO () +handleFacts args + | args `Cli.has` Cli.command "list" = do + let maybeProject = Text.pack putText "fact-id required" + Just fidStr -> case readMaybe fidStr of + Nothing -> putText "Invalid fact ID (must be integer)" + Just fid -> do + maybeFact <- Fact.getFact fid + case maybeFact of + Nothing -> putText "Fact not found" + Just fact -> + if jsonMode + then BLC.putStrLn (Aeson.encode fact) + else printFactDetailed fact + | args `Cli.has` Cli.command "add" = do + let jsonMode = args `Cli.has` Cli.longOption "json" + case (Cli.getArg args (Cli.argument "project"), Cli.getArg args (Cli.argument "content")) of + (Just proj, Just content) -> do + let files = case Cli.getArg args (Cli.longOption "files") of + Just f -> Text.splitOn "," (Text.pack f) + Nothing -> [] + sourceTask = Text.pack fromMaybe 0.8 (readMaybe c) + Nothing -> 0.8 + factId <- Fact.createFact (Text.pack proj) (Text.pack content) files sourceTask confidence + if jsonMode + then BLC.putStrLn (Aeson.encode (Aeson.object ["id" Aeson..= factId, "success" Aeson..= True])) + else putText ("Created fact: " <> tshow factId) + _ -> putText "project and content required" + | args `Cli.has` Cli.command "delete" = do + let jsonMode = args `Cli.has` Cli.longOption "json" + case Cli.getArg args (Cli.argument "fact-id") of + Nothing -> putText "fact-id required" + Just fidStr -> case readMaybe fidStr of + Nothing -> putText "Invalid fact ID (must be integer)" + Just fid -> do + Fact.deleteFact fid + if jsonMode + then BLC.putStrLn (Aeson.encode (Aeson.object ["success" Aeson..= True, "message" Aeson..= ("Deleted fact " <> tshow fid)])) + else putText ("Deleted fact: " <> tshow fid) + | otherwise = putText "Unknown facts subcommand. Use: list, show, add, or delete" + +-- | Print a fact in a compact format +printFact :: TaskCore.Fact -> IO () +printFact fact = do + let fid = maybe "?" tshow (TaskCore.factId fact) + proj = TaskCore.factProject fact + content = Text.take 60 (TaskCore.factContent fact) + suffix = if Text.length (TaskCore.factContent fact) > 60 then "..." else "" + putText (fid <> "\t" <> proj <> "\t" <> content <> suffix) + +-- | Print a fact in detailed format +printFactDetailed :: TaskCore.Fact -> IO () +printFactDetailed fact = do + putText ("ID: " <> maybe "?" tshow (TaskCore.factId fact)) + putText ("Project: " <> TaskCore.factProject fact) + putText ("Content: " <> TaskCore.factContent fact) + putText ("Files: " <> Text.intercalate ", " (TaskCore.factRelatedFiles fact)) + putText ("Source: " <> fromMaybe "-" (TaskCore.factSourceTask fact)) + putText ("Confidence: " <> tshow (TaskCore.factConfidence fact)) + putText ("Created: " <> tshow (TaskCore.factCreatedAt fact)) + test :: Test.Tree test = Test.group @@ -535,5 +622,63 @@ test = Left err -> Test.assertFailure <| "Failed to parse 'work t-123': " <> show err Right args -> do args `Cli.has` Cli.command "work" Test.@?= True - Cli.getArg args (Cli.argument "task-id") Test.@?= Just "t-123" + Cli.getArg args (Cli.argument "task-id") Test.@?= Just "t-123", + Test.unit "can parse facts list command" <| do + let result = Docopt.parseArgs help ["facts", "list"] + case result of + Left err -> Test.assertFailure <| "Failed to parse 'facts list': " <> show err + Right args -> do + args `Cli.has` Cli.command "facts" Test.@?= True + args `Cli.has` Cli.command "list" Test.@?= True, + Test.unit "can parse facts list with --project" <| do + let result = Docopt.parseArgs help ["facts", "list", "--project=myproj"] + case result of + Left err -> Test.assertFailure <| "Failed to parse 'facts list --project': " <> show err + Right args -> do + args `Cli.has` Cli.command "facts" Test.@?= True + args `Cli.has` Cli.command "list" Test.@?= True + Cli.getArg args (Cli.longOption "project") Test.@?= Just "myproj", + Test.unit "can parse facts list with --json" <| do + let result = Docopt.parseArgs help ["facts", "list", "--json"] + case result of + Left err -> Test.assertFailure <| "Failed to parse 'facts list --json': " <> show err + Right args -> do + args `Cli.has` Cli.command "facts" Test.@?= True + args `Cli.has` Cli.command "list" Test.@?= True + args `Cli.has` Cli.longOption "json" Test.@?= True, + Test.unit "can parse facts show command" <| do + let result = Docopt.parseArgs help ["facts", "show", "42"] + case result of + Left err -> Test.assertFailure <| "Failed to parse 'facts show 42': " <> show err + Right args -> do + args `Cli.has` Cli.command "facts" Test.@?= True + args `Cli.has` Cli.command "show" Test.@?= True + Cli.getArg args (Cli.argument "fact-id") Test.@?= Just "42", + Test.unit "can parse facts add command" <| do + let result = Docopt.parseArgs help ["facts", "add", "myproj", "This is a fact"] + case result of + Left err -> Test.assertFailure <| "Failed to parse 'facts add': " <> show err + Right args -> do + args `Cli.has` Cli.command "facts" Test.@?= True + args `Cli.has` Cli.command "add" Test.@?= True + Cli.getArg args (Cli.argument "project") Test.@?= Just "myproj" + Cli.getArg args (Cli.argument "content") Test.@?= Just "This is a fact", + Test.unit "can parse facts add with options" <| do + let result = Docopt.parseArgs help ["facts", "add", "myproj", "fact", "--files=a.hs,b.hs", "--task=t-123", "--confidence=0.9"] + case result of + Left err -> Test.assertFailure <| "Failed to parse 'facts add' with options: " <> show err + Right args -> do + args `Cli.has` Cli.command "facts" Test.@?= True + args `Cli.has` Cli.command "add" Test.@?= True + Cli.getArg args (Cli.longOption "files") Test.@?= Just "a.hs,b.hs" + Cli.getArg args (Cli.longOption "task") Test.@?= Just "t-123" + Cli.getArg args (Cli.longOption "confidence") Test.@?= Just "0.9", + Test.unit "can parse facts delete command" <| do + let result = Docopt.parseArgs help ["facts", "delete", "42"] + case result of + Left err -> Test.assertFailure <| "Failed to parse 'facts delete 42': " <> show err + Right args -> do + args `Cli.has` Cli.command "facts" Test.@?= True + args `Cli.has` Cli.command "delete" Test.@?= True + Cli.getArg args (Cli.argument "fact-id") Test.@?= Just "42" ] -- cgit v1.2.3