···258258 assert "--day" in captured_argv
259259 assert "20250101" in captured_argv
260260261261- def test_main_help_command_with_question_dispatches(self, monkeypatch):
262262- """Test 'help' with extra args dispatches to help module."""
263263- monkeypatch.setattr(sys, "argv", ["sol", "help", "how", "do", "I", "search"])
264264-265265- captured_argv = []
266266-267267- def mock_main():
268268- captured_argv.extend(sys.argv)
269269-270270- mock_module = MagicMock()
271271- mock_module.main = mock_main
272272-273273- with patch("importlib.import_module", return_value=mock_module):
274274- with pytest.raises(SystemExit):
275275- sol.main()
276276-277277- assert captured_argv[0] == "sol help"
278278- assert "how" in captured_argv
279279- assert "search" in captured_argv
280280-281281-282261class TestCommandRegistry:
283262 """Tests for command registry completeness."""
284263···297276298277 def test_critical_commands_registered(self):
299278 """Test that critical commands are registered."""
300300- critical = ["import", "agents", "dream", "indexer", "transcribe", "help"]
279279+ critical = ["import", "agents", "dream", "indexer", "transcribe"]
301280 for cmd in critical:
302281 assert cmd in sol.COMMANDS, f"Critical command '{cmd}' not registered"
-57
think/callosum.py
···556556 else:
557557 print("Failed to send (is callosum running?)", file=sys.stderr)
558558 sys.exit(1)
559559-560560-561561-def main() -> None:
562562- """CLI entry point for Callosum message bus tools."""
563563- import argparse
564564-565565- from think.utils import setup_cli
566566-567567- parser = argparse.ArgumentParser(
568568- description="Callosum message bus tools",
569569- epilog="Run 'sol callosum' with no subcommand to listen to all events.",
570570- )
571571- subparsers = parser.add_subparsers(dest="subcommand")
572572-573573- # --- listen subcommand ---
574574- listen_parser = subparsers.add_parser(
575575- "listen", help="Listen to events on the message bus"
576576- )
577577- listen_parser.add_argument("--tract", help="Filter to a specific tract")
578578- listen_parser.add_argument("--event", help="Filter to a specific event type")
579579- listen_parser.add_argument(
580580- "-p", "--pretty", action="store_true", help="Pretty-print JSON output"
581581- )
582582-583583- # --- send subcommand ---
584584- send_parser = subparsers.add_parser(
585585- "send",
586586- help="Send a message to the bus",
587587- epilog=(
588588- "Examples:\n"
589589- " sol callosum send observe described day=20250101 segment=143045_300\n"
590590- ' sol callosum send \'{"tract":"test","event":"ping"}\'\n'
591591- " echo '{...}' | sol callosum send"
592592- ),
593593- formatter_class=argparse.RawDescriptionHelpFormatter,
594594- )
595595- send_parser.add_argument(
596596- "args", nargs="*", help="tract event [key=value ...] or JSON string"
597597- )
598598-599599- args = setup_cli(parser)
600600-601601- if args.subcommand == "send":
602602- _cmd_send(args)
603603- else:
604604- # Default: listen (both bare 'sol callosum' and 'sol callosum listen')
605605- if not hasattr(args, "tract"):
606606- args.tract = None
607607- if not hasattr(args, "event"):
608608- args.event = None
609609- if not hasattr(args, "pretty"):
610610- args.pretty = False
611611- _cmd_listen(args)
612612-613613-614614-if __name__ == "__main__":
615615- main()
-57
think/config_cli.py
···11-# SPDX-License-Identifier: AGPL-3.0-only
22-# Copyright (c) 2026 sol pbc
33-44-"""CLI for inspecting journal configuration.
55-66-Shows the resolved journal configuration as JSON, or prints the journal path.
77-88-Usage:
99- sol config Show full config JSON
1010- sol config env Show journal path
1111- sol config facet rename OLD NEW Rename a facet
1212-"""
1313-1414-from __future__ import annotations
1515-1616-import argparse
1717-import json
1818-import sys
1919-2020-from think.utils import get_config, get_journal_info, setup_cli
2121-2222-2323-def main() -> None:
2424- parser = argparse.ArgumentParser(description="Show journal configuration")
2525- subparsers = parser.add_subparsers(dest="subcommand")
2626- subparsers.add_parser("env", help="Show journal path and source")
2727-2828- # facet subcommand with its own sub-subcommands
2929- facet_parser = subparsers.add_parser("facet", help="Facet management")
3030- facet_sub = facet_parser.add_subparsers(dest="facet_action")
3131- rename_parser = facet_sub.add_parser("rename", help="Rename a facet")
3232- rename_parser.add_argument("old_name", help="Current facet name")
3333- rename_parser.add_argument("new_name", help="New facet name")
3434-3535- args = setup_cli(parser)
3636-3737- if args.subcommand == "env":
3838- path, _source = get_journal_info()
3939- print(path)
4040- elif args.subcommand == "facet":
4141- if args.facet_action == "rename":
4242- from think.facets import rename_facet
4343-4444- try:
4545- rename_facet(args.old_name, args.new_name)
4646- except ValueError as exc:
4747- print(f"Error: {exc}", file=sys.stderr)
4848- sys.exit(1)
4949- else:
5050- facet_parser.print_help()
5151- else:
5252- config = get_config()
5353- print(json.dumps(config, indent=2))
5454-5555-5656-if __name__ == "__main__":
5757- main()
-25
think/detect_created.py
···124124 return result
125125 except json.JSONDecodeError:
126126 return None
127127-128128-129129-def main():
130130- """Main CLI entry point for detect_created utility."""
131131- import argparse
132132-133133- from .utils import setup_cli
134134-135135- parser = argparse.ArgumentParser(
136136- description="Detect creation time information from media file metadata"
137137- )
138138- parser.add_argument("file_path", help="Path to the media file to analyze")
139139-140140- args = setup_cli(parser)
141141-142142- result = detect_created(args.file_path)
143143- if result is not None:
144144- print(json.dumps(result, indent=2))
145145- else:
146146- print("Failed to detect creation time information", file=sys.stderr)
147147- sys.exit(1)
148148-149149-150150-if __name__ == "__main__":
151151- main()
-90
think/formatters.py
···3333Markdown formatters receive str text and perform semantic chunking.
3434"""
35353636-import argparse
3736import fnmatch
3837import json
3938import os
4040-import sys
4139from importlib import import_module
4240from pathlib import Path
4341from typing import Any, Callable
···363361 print(f' intro: "{intro[:60]}{"..." if len(intro) > 60 else ""}"')
364362 print(f" {preview[:70]}{'...' if len(preview) > 70 else ''}")
365363 print()
366366-367367-368368-def main() -> None:
369369- """CLI entry point for sol formatter."""
370370- from think.utils import setup_cli
371371-372372- parser = argparse.ArgumentParser(
373373- description="Convert JSONL or Markdown files to formatted chunks"
374374- )
375375- parser.add_argument("file", help="Path to JSONL or Markdown file")
376376- parser.add_argument(
377377- "-f",
378378- "--format",
379379- choices=["json", "markdown", "summary"],
380380- default="json",
381381- help="Output format (default: json)",
382382- )
383383- parser.add_argument(
384384- "-i",
385385- "--index",
386386- type=int,
387387- help="Show only the chunk at this index",
388388- )
389389- parser.add_argument(
390390- "--join",
391391- action="store_true",
392392- help="Output concatenated markdown (shorthand for --format=markdown)",
393393- )
394394- parser.add_argument(
395395- "--context",
396396- type=str,
397397- help="JSON string of context to pass to formatter",
398398- )
399399- args = setup_cli(parser)
400400-401401- # --join is shorthand for --format=markdown
402402- if args.join:
403403- args.format = "markdown"
404404-405405- try:
406406- context = json.loads(args.context) if args.context else None
407407- except json.JSONDecodeError as e:
408408- print(f"Error parsing context JSON: {e}", file=sys.stderr)
409409- sys.exit(1)
410410-411411- try:
412412- chunks, meta = format_file(args.file, context)
413413- except (ValueError, FileNotFoundError) as e:
414414- print(f"Error: {e}", file=sys.stderr)
415415- sys.exit(1)
416416-417417- # For summary format on markdown files, get raw chunks with metadata
418418- raw_chunks = None
419419- if args.format == "summary" and args.file.endswith(".md"):
420420- from think.markdown import chunk_markdown
421421-422422- text = load_markdown(args.file)
423423- raw_chunks = chunk_markdown(text)
424424-425425- # Filter to single chunk if requested
426426- if args.index is not None:
427427- if 0 <= args.index < len(chunks):
428428- chunks = [chunks[args.index]]
429429- if raw_chunks:
430430- raw_chunks = [raw_chunks[args.index]]
431431- else:
432432- print(
433433- f"Error: Index {args.index} out of range (0-{len(chunks) - 1})",
434434- file=sys.stderr,
435435- )
436436- sys.exit(1)
437437-438438- if args.format == "markdown":
439439- # Output concatenated markdown with header first
440440- parts = []
441441- if meta.get("header"):
442442- parts.append(meta["header"])
443443- parts.extend(chunk["markdown"] for chunk in chunks)
444444- print("\n".join(parts))
445445- elif args.format == "summary":
446446- _format_chunk_summary(chunks, raw_chunks)
447447- else:
448448- # Output JSON object with metadata and chunks
449449- print(json.dumps({"meta": meta, "chunks": chunks}, indent=2))
450450-451451-452452-if __name__ == "__main__":
453453- main()
-122
think/help_cli.py
···11-# SPDX-License-Identifier: AGPL-3.0-only
22-# Copyright (c) 2026 sol pbc
33-44-"""CLI command for interactive help with sol commands."""
55-66-from __future__ import annotations
77-88-import argparse
99-import json
1010-import subprocess
1111-import sys
1212-1313-from think.utils import setup_cli
1414-1515-1616-def _read_stdin() -> str:
1717- """Read a question from stdin. Shows a prompt if running in a terminal."""
1818- if sys.stdin.isatty():
1919- print(
2020- "Enter your question (Ctrl+D to submit):",
2121- file=sys.stderr,
2222- )
2323- try:
2424- return sys.stdin.read().strip()
2525- except KeyboardInterrupt:
2626- return ""
2727-2828-2929-def main() -> None:
3030- """Entry point for ``sol help``."""
3131- parser = argparse.ArgumentParser(
3232- prog="sol help",
3333- description="Get help with sol commands",
3434- )
3535- parser.add_argument(
3636- "question",
3737- nargs="*",
3838- help="Question about sol commands",
3939- )
4040-4141- args = setup_cli(parser)
4242-4343- if not args.question:
4444- question = _read_stdin()
4545- if not question:
4646- # Imported here to avoid circular import (sol.py imports think.help_cli).
4747- from sol import print_help
4848-4949- print_help()
5050- return
5151- else:
5252- question = " ".join(args.question).strip()
5353-5454- config = {"name": "unified", "prompt": question}
5555- config_json = json.dumps(config)
5656-5757- print("Thinking...", end="", file=sys.stderr, flush=True)
5858-5959- try:
6060- proc = subprocess.Popen(
6161- ["sol", "agents"],
6262- stdin=subprocess.PIPE,
6363- stdout=subprocess.PIPE,
6464- stderr=subprocess.PIPE,
6565- text=True,
6666- )
6767- except Exception as exc:
6868- print(f"\rError: failed to run help agent: {exc}", file=sys.stderr)
6969- sys.exit(1)
7070-7171- assert proc.stdin is not None # for type checker
7272- proc.stdin.write(config_json + "\n")
7373- proc.stdin.close()
7474-7575- finish_result: str | None = None
7676- errors: list[str] = []
7777-7878- assert proc.stdout is not None # for type checker
7979- for line in proc.stdout:
8080- line = line.strip()
8181- if not line:
8282- continue
8383-8484- try:
8585- event = json.loads(line)
8686- except json.JSONDecodeError:
8787- continue
8888-8989- event_type = event.get("event")
9090- if event_type == "error":
9191- errors.append(str(event.get("error", "Unknown error")))
9292- elif event_type == "finish":
9393- result_value = event.get("result")
9494- finish_result = "" if result_value is None else str(result_value)
9595-9696- try:
9797- proc.wait(timeout=120)
9898- except subprocess.TimeoutExpired:
9999- proc.kill()
100100- print("\rError: help request timed out after 120 seconds.", file=sys.stderr)
101101- sys.exit(1)
102102-103103- # Clear the "Thinking..." indicator.
104104- print("\r \r", end="", file=sys.stderr, flush=True)
105105-106106- for message in errors:
107107- print(f"Error: {message}", file=sys.stderr)
108108-109109- if finish_result is not None and finish_result.strip():
110110- print(finish_result)
111111- return
112112-113113- if finish_result is not None:
114114- print("Error: help agent returned an empty result.", file=sys.stderr)
115115- sys.exit(1)
116116-117117- stderr_output = proc.stderr.read() if proc.stderr else ""
118118- if proc.returncode != 0 and stderr_output.strip():
119119- print(f"Error: {stderr_output.strip()}", file=sys.stderr)
120120- else:
121121- print("Error: no help response received.", file=sys.stderr)
122122- sys.exit(1)
-39
think/planner.py
···11# SPDX-License-Identifier: AGPL-3.0-only
22# Copyright (c) 2026 sol pbc
3344-import argparse
55-import os
66-import sys
74from pathlib import Path
8596from .prompts import load_prompt
1010-from .utils import setup_cli
117128139def _load_prompt() -> str:
···2824 thinking_budget=4096,
2925 system_instruction=_load_prompt(),
3026 )
3131-3232-3333-def parse_args() -> argparse.ArgumentParser:
3434- parser = argparse.ArgumentParser(
3535- description="Generate an agent plan using configured provider"
3636- )
3737- parser.add_argument(
3838- "task",
3939- nargs="?",
4040- help="Path to .txt file with the request or '-' for stdin",
4141- )
4242- parser.add_argument("-q", "--query", help="Request text directly")
4343- return parser
4444-4545-4646-def main() -> None:
4747- parser = parse_args()
4848- args = setup_cli(parser)
4949- if args.query:
5050- request = args.query
5151- elif args.task is None:
5252- parser.error("request not provided")
5353- elif args.task == "-":
5454- request = sys.stdin.read()
5555- else:
5656- if not os.path.isfile(args.task):
5757- parser.error(f"File not found: {args.task}")
5858- request = Path(args.task).read_text(encoding="utf-8")
5959-6060- plan = generate_plan(request)
6161- print(plan)
6262-6363-6464-if __name__ == "__main__": # pragma: no cover - manual execution
6565- main()