personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add sol help command with CLI help cogitate agent

Adds `sol help "question"` which invokes a cogitate agent to answer
questions about sol commands, subcommands, and arguments. The agent
prompt embeds a complete command reference covering all sol commands
and sol call tool commands.

`sol help` (no args), `sol --help`, and `sol -h` continue to show
the existing static help output.

+401 -2
+108
muse/help.md
··· 1 + { 2 + "type": "cogitate", 3 + "title": "CLI Help", 4 + "description": "Answer questions about sol commands and usage", 5 + "instructions": {} 6 + } 7 + 8 + You are the sol CLI help assistant. Answer the user's question with specific sol commands, subcommands, and arguments. 9 + 10 + Guidelines: 11 + - Be concise and practical. 12 + - Suggest concrete commands with example arguments. 13 + - Explain what each command does. 14 + - Format responses as clear, readable text. 15 + - Use backticks for commands. 16 + - If the question is unclear, suggest the most likely relevant commands and ask the user to be more specific. 17 + 18 + ## Core `sol` Command Reference 19 + 20 + ### Think (daily processing) 21 + - `sol import` - Import data into the journal. 22 + - `sol dream` - Run daily processing workflows for a day. 23 + - `sol planner` - Run planning workflows. 24 + - `sol indexer` - Build/update the journal index. 25 + - `sol supervisor` - Run supervisor services. 26 + - `sol detect-created` - Detect newly created content artifacts. 27 + - `sol top` - Show runtime/service activity status. 28 + - `sol logs` - View service health logs. 29 + - `sol callosum` - Interact with Callosum message bus tooling. 30 + - `sol streams` - Manage or inspect stream-related state. 31 + - `sol journal-stats` - Show journal statistics. 32 + - `sol config` - Inspect or manage configuration. 33 + - `sol formatter` - Run formatter utilities. 34 + 35 + ### Observe (capture) 36 + - `sol transcribe` - Transcribe captured audio. 37 + - `sol describe` - Describe visual captures. 38 + - `sol sense` - Run multimodal sensing pipeline. 39 + - `sol sync` - Sync capture artifacts. 40 + - `sol transfer` - Transfer capture data. 41 + - `sol observer` - Run observer capture process. 42 + - `sol observe-linux` - Linux observer entry point. 43 + - `sol observe-macos` - macOS observer entry point. 44 + 45 + ### Muse (AI agents) 46 + - `sol agents` - Unified NDJSON agent CLI (tool agents + generators). 47 + - `sol cortex` - Orchestrate agent execution. 48 + - `sol muse` - Inspect muse agents/generators and run logs. 49 + - `sol call` - Run app/built-in call subcommands. 50 + 51 + ### Convey (web UI) 52 + - `sol convey` - Run Convey web application services. 53 + - `sol restart-convey` - Restart Convey service. 54 + - `sol screenshot` - Capture Convey screenshots for routes. 55 + - `sol maint` - Run Convey maintenance commands. 56 + 57 + ### Help and aliases 58 + - `sol help "question"` - Ask this help assistant how to use commands. 59 + - `sol start` - Alias for `sol supervisor`. 60 + 61 + ## `sol muse` Commands 62 + - `sol muse` - List prompts grouped by schedule. 63 + - `sol muse list` - List prompts with optional filters. 64 + - `sol muse show <name>` - Show details for one prompt. 65 + - `sol muse logs` - Show recent agent run logs. 66 + - `sol muse log <id>` - Show events for one run. 67 + 68 + ## `sol call` Command Reference 69 + 70 + ### Journal 71 + - `sol call journal search [query] [-n limit] [--offset N] [-d YYYYMMDD] [--day-from YYYYMMDD] [--day-to YYYYMMDD] [-f facet] [-t topic]` - Search journal entries. 72 + - `sol call journal events <day> [-f facet]` - List events for a day. 73 + - `sol call journal facet <name>` - Show facet details. 74 + - `sol call journal facets` - List all facets. 75 + - `sol call journal news <name> [-d YYYYMMDD] [-n limit] [--cursor CURSOR] [-w]` - Get news feed for a facet. 76 + - `sol call journal topics <day> [-s HHMMSS_LEN]` - List topics for a day. 77 + - `sol call journal read <day> <topic> [-s HHMMSS_LEN] [--max N]` - Read transcript content for a topic. 78 + 79 + ### Entities 80 + - `sol call entities list <facet> [-d day]` - List entities in a facet. 81 + - `sol call entities detect <day> <facet> <TYPE> <entity> <description>` - Detect/record an entity. 82 + - `sol call entities attach <facet> <TYPE> <entity> <description>` - Attach entity to facet. 83 + - `sol call entities update <facet> <entity> <description> [-d day]` - Update entity description. 84 + - `sol call entities aka <facet> <entity> <AKA>` - Add alias for entity. 85 + - `sol call entities observations <facet> <entity>` - List observations for entity. 86 + - `sol call entities observe <facet> <entity> <content> [--source-day YYYYMMDD]` - Record observation. 87 + 88 + ### Todos 89 + - `sol call todos list <day> [-f facet] [--to end_day]` - List todos. 90 + - `sol call todos add <day> <text> --facet/-f <facet>` - Add a todo. 91 + - `sol call todos done <day> <line_number> --facet/-f <facet>` - Complete a todo. 92 + - `sol call todos cancel <day> <line_number> --facet/-f <facet>` - Cancel a todo. 93 + - `sol call todos upcoming [-l limit] [-f facet]` - Show upcoming todos. 94 + 95 + ### Transcripts 96 + - `sol call transcripts scan <day>` - Scan recordings for a day. 97 + - `sol call transcripts segments <day>` - List transcript segments. 98 + - `sol call transcripts read <day> [--start HHMMSS] [--length MINUTES] [--segment HHMMSS_LEN] [--stream NAME] [--full] [--raw] [--audio] [--screen] [--agents] [--max N]` - Read transcript text. 99 + - `sol call transcripts stats <month>` - Show transcript statistics. 100 + 101 + ## Example Answers 102 + - If asked "How do I search journal entries?": 103 + - Use `sol call journal search "query"` for broad search. 104 + - Add `-d YYYYMMDD` to focus one day. 105 + - Add `-t audio` or `-t flow` to narrow by topic. 106 + - If asked "How do I inspect an agent run?": 107 + - Use `sol muse logs` to find run IDs. 108 + - Use `sol muse log <id>` for event details.
+6 -1
sol.py
··· 65 65 "cortex": "think.cortex", 66 66 "muse": "think.muse_cli", 67 67 "call": "think.call", 68 + "help": "think.help_cli", 68 69 # convey package - web UI 69 70 "convey": "convey.cli", 70 71 "restart-convey": "convey.restart", ··· 127 128 "observe-linux", 128 129 "observe-macos", 129 130 ], 131 + "Help": ["help"], 130 132 } 131 133 132 134 ··· 273 275 cmd = sys.argv[1] 274 276 275 277 # Help flags 276 - if cmd in ("--help", "-h", "help"): 278 + if cmd in ("--help", "-h"): 279 + print_help() 280 + return 281 + if cmd == "help" and len(sys.argv) <= 2: 277 282 print_help() 278 283 return 279 284
+164
tests/test_help_cli.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Tests for think.help_cli.""" 5 + 6 + import json 7 + import subprocess 8 + import sys 9 + from unittest.mock import patch 10 + 11 + import pytest 12 + 13 + from think.help_cli import main 14 + 15 + 16 + @pytest.fixture(autouse=True) 17 + def _set_journal_path(monkeypatch): 18 + monkeypatch.setenv("JOURNAL_PATH", "tests/fixtures/journal") 19 + 20 + 21 + def test_help_no_question_shows_static_help(monkeypatch): 22 + monkeypatch.setattr(sys, "argv", ["sol help"]) 23 + 24 + with patch("sol.print_help") as mock_print_help: 25 + main() 26 + 27 + mock_print_help.assert_called_once() 28 + 29 + 30 + def test_help_parses_question(monkeypatch): 31 + monkeypatch.setattr(sys, "argv", ["sol help", "how", "do", "I", "search"]) 32 + mock_result = subprocess.CompletedProcess( 33 + args=["sol", "agents"], 34 + returncode=0, 35 + stdout='{"event":"finish","result":"Use sol call journal search"}\n', 36 + stderr="", 37 + ) 38 + 39 + with patch("think.help_cli.subprocess.run", return_value=mock_result) as mock_run: 40 + main() 41 + 42 + call_args = mock_run.call_args 43 + assert call_args[0][0] == ["sol", "agents"] 44 + assert call_args[1]["capture_output"] is True 45 + assert call_args[1]["text"] is True 46 + assert call_args[1]["timeout"] == 120 47 + 48 + sent = call_args[1]["input"] 49 + payload = json.loads(sent.strip()) 50 + assert payload["prompt"] == "how do I search" 51 + 52 + 53 + def test_help_ndjson_config(monkeypatch): 54 + monkeypatch.setattr(sys, "argv", ["sol help", "show", "todo", "commands"]) 55 + mock_result = subprocess.CompletedProcess( 56 + args=["sol", "agents"], 57 + returncode=0, 58 + stdout='{"event":"finish","result":"ok"}\n', 59 + stderr="", 60 + ) 61 + 62 + with patch("think.help_cli.subprocess.run", return_value=mock_result) as mock_run: 63 + main() 64 + 65 + sent = mock_run.call_args[1]["input"] 66 + payload = json.loads(sent.strip()) 67 + assert payload == {"name": "help", "prompt": "show todo commands"} 68 + 69 + 70 + def test_help_parses_finish_event(monkeypatch, capsys): 71 + monkeypatch.setattr(sys, "argv", ["sol help", "how", "to", "search"]) 72 + stdout = "\n".join( 73 + [ 74 + '{"event":"start","ts":1}', 75 + '{"event":"thinking","ts":2,"summary":"..."}', 76 + '{"event":"finish","ts":3,"result":"Use `sol call journal search`."}', 77 + ] 78 + ) 79 + mock_result = subprocess.CompletedProcess( 80 + args=["sol", "agents"], 81 + returncode=0, 82 + stdout=stdout, 83 + stderr="", 84 + ) 85 + 86 + with patch("think.help_cli.subprocess.run", return_value=mock_result): 87 + main() 88 + 89 + captured = capsys.readouterr() 90 + assert "Use `sol call journal search`." in captured.out 91 + 92 + 93 + def test_help_uses_last_finish_event(monkeypatch, capsys): 94 + monkeypatch.setattr(sys, "argv", ["sol help", "search"]) 95 + stdout = "\n".join( 96 + [ 97 + '{"event":"finish","ts":1,"result":"old result"}', 98 + '{"event":"finish","ts":2,"result":"new result"}', 99 + ] 100 + ) 101 + mock_result = subprocess.CompletedProcess( 102 + args=["sol", "agents"], 103 + returncode=0, 104 + stdout=stdout, 105 + stderr="", 106 + ) 107 + 108 + with patch("think.help_cli.subprocess.run", return_value=mock_result): 109 + main() 110 + 111 + captured = capsys.readouterr() 112 + assert "new result" in captured.out 113 + assert "old result" not in captured.out 114 + 115 + 116 + def test_help_handles_error_event(monkeypatch, capsys): 117 + monkeypatch.setattr(sys, "argv", ["sol help", "bad", "request"]) 118 + mock_result = subprocess.CompletedProcess( 119 + args=["sol", "agents"], 120 + returncode=1, 121 + stdout='{"event":"error","error":"provider unavailable"}\n', 122 + stderr="provider failed", 123 + ) 124 + 125 + with patch("think.help_cli.subprocess.run", return_value=mock_result): 126 + with pytest.raises(SystemExit) as exc_info: 127 + main() 128 + 129 + assert exc_info.value.code == 1 130 + captured = capsys.readouterr() 131 + assert "provider unavailable" in captured.err 132 + 133 + 134 + def test_help_handles_empty_finish_result(monkeypatch, capsys): 135 + monkeypatch.setattr(sys, "argv", ["sol help", "empty"]) 136 + mock_result = subprocess.CompletedProcess( 137 + args=["sol", "agents"], 138 + returncode=0, 139 + stdout='{"event":"finish","result":""}\n', 140 + stderr="", 141 + ) 142 + 143 + with patch("think.help_cli.subprocess.run", return_value=mock_result): 144 + with pytest.raises(SystemExit) as exc_info: 145 + main() 146 + 147 + assert exc_info.value.code == 1 148 + captured = capsys.readouterr() 149 + assert "empty result" in captured.err.lower() 150 + 151 + 152 + def test_help_handles_timeout(monkeypatch, capsys): 153 + monkeypatch.setattr(sys, "argv", ["sol help", "slow", "question"]) 154 + 155 + with patch( 156 + "think.help_cli.subprocess.run", 157 + side_effect=subprocess.TimeoutExpired(cmd=["sol", "agents"], timeout=120), 158 + ): 159 + with pytest.raises(SystemExit) as exc_info: 160 + main() 161 + 162 + assert exc_info.value.code == 1 163 + captured = capsys.readouterr() 164 + assert "timed out" in captured.err.lower()
+31 -1
tests/test_sol.py
··· 175 175 captured = capsys.readouterr() 176 176 assert "sol - solstone unified CLI" in captured.out 177 177 178 + def test_main_help_command_without_question(self, monkeypatch, capsys): 179 + """Test bare 'help' command shows static help.""" 180 + monkeypatch.setattr(sys, "argv", ["sol", "help"]) 181 + monkeypatch.setenv("JOURNAL_PATH", "/tmp/test") 182 + 183 + sol.main() 184 + 185 + captured = capsys.readouterr() 186 + assert "sol - solstone unified CLI" in captured.out 187 + 178 188 def test_main_version_flag(self, monkeypatch, capsys): 179 189 """Test --version flag shows version.""" 180 190 monkeypatch.setattr(sys, "argv", ["sol", "--version"]) ··· 212 222 assert "--day" in captured_argv 213 223 assert "20250101" in captured_argv 214 224 225 + def test_main_help_command_with_question_dispatches(self, monkeypatch): 226 + """Test 'help' with extra args dispatches to help module.""" 227 + monkeypatch.setattr(sys, "argv", ["sol", "help", "how", "do", "I", "search"]) 228 + 229 + captured_argv = [] 230 + 231 + def mock_main(): 232 + captured_argv.extend(sys.argv) 233 + 234 + mock_module = MagicMock() 235 + mock_module.main = mock_main 236 + 237 + with patch("importlib.import_module", return_value=mock_module): 238 + with pytest.raises(SystemExit): 239 + sol.main() 240 + 241 + assert captured_argv[0] == "sol help" 242 + assert "how" in captured_argv 243 + assert "search" in captured_argv 244 + 215 245 216 246 class TestCommandRegistry: 217 247 """Tests for command registry completeness.""" ··· 231 261 232 262 def test_critical_commands_registered(self): 233 263 """Test that critical commands are registered.""" 234 - critical = ["import", "agents", "dream", "indexer", "transcribe"] 264 + critical = ["import", "agents", "dream", "indexer", "transcribe", "help"] 235 265 for cmd in critical: 236 266 assert cmd in sol.COMMANDS, f"Critical command '{cmd}' not registered"
+92
think/help_cli.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """CLI command for interactive help with sol commands.""" 5 + 6 + from __future__ import annotations 7 + 8 + import argparse 9 + import json 10 + import subprocess 11 + import sys 12 + 13 + from think.utils import setup_cli 14 + 15 + 16 + def main() -> None: 17 + """Entry point for ``sol help``.""" 18 + parser = argparse.ArgumentParser( 19 + prog="sol help", 20 + description="Get help with sol commands", 21 + ) 22 + parser.add_argument( 23 + "question", 24 + nargs="*", 25 + help="Question about sol commands", 26 + ) 27 + 28 + args = setup_cli(parser) 29 + 30 + if not args.question: 31 + # Imported here to avoid circular import (sol.py imports think.help_cli). 32 + from sol import print_help 33 + 34 + print_help() 35 + return 36 + 37 + question = " ".join(args.question).strip() 38 + config = {"name": "help", "prompt": question} 39 + config_json = json.dumps(config) 40 + 41 + try: 42 + result = subprocess.run( 43 + ["sol", "agents"], 44 + input=config_json + "\n", 45 + capture_output=True, 46 + text=True, 47 + timeout=120, 48 + ) 49 + except subprocess.TimeoutExpired: 50 + print("Error: help request timed out after 120 seconds.", file=sys.stderr) 51 + sys.exit(1) 52 + except Exception as exc: 53 + print(f"Error: failed to run help agent: {exc}", file=sys.stderr) 54 + sys.exit(1) 55 + 56 + finish_result: str | None = None 57 + errors: list[str] = [] 58 + 59 + for line in result.stdout.splitlines(): 60 + line = line.strip() 61 + if not line: 62 + continue 63 + 64 + try: 65 + event = json.loads(line) 66 + except json.JSONDecodeError: 67 + continue 68 + 69 + event_type = event.get("event") 70 + if event_type == "error": 71 + errors.append(str(event.get("error", "Unknown error"))) 72 + elif event_type == "finish": 73 + # Keep the last finish result seen in the stream. 74 + result_value = event.get("result") 75 + finish_result = "" if result_value is None else str(result_value) 76 + 77 + for message in errors: 78 + print(f"Error: {message}", file=sys.stderr) 79 + 80 + if finish_result is not None and finish_result.strip(): 81 + print(finish_result) 82 + return 83 + 84 + if finish_result is not None: 85 + print("Error: help agent returned an empty result.", file=sys.stderr) 86 + sys.exit(1) 87 + 88 + if result.returncode != 0 and result.stderr.strip(): 89 + print(f"Error: {result.stderr.strip()}", file=sys.stderr) 90 + else: 91 + print("Error: no help response received.", file=sys.stderr) 92 + sys.exit(1)