personal memory agent
0
fork

Configure Feed

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

Add SOL_* env var injection for sol call commands

Dream now injects SOL_DAY, SOL_FACET, SOL_SEGMENT, SOL_STREAM, and
SOL_ACTIVITY as environment variables into cortex agent requests. All
sol call commands (journal, todos, entities, transcripts) resolve these
from env when CLI args aren't provided, eliminating the need for agents
to explicitly pass day/facet/segment in every invocation.

Key changes:
- Add resolve helpers in think/utils.py (resolve_sol_day, etc.)
- Normalize SEGMENT_KEY → SOL_SEGMENT, STREAM_NAME → SOL_STREAM
- Convert day from positional to --day option where required positionals
follow (journal read, todos add/done/cancel, entities detect)
- Inject SOL_DAY/SOL_FACET unconditionally for all agent schedules
- Update SKILL.md docs and agent prompts for new CLI signatures
- Add env resolution tests across all call modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+573 -114
+1 -1
apps/agents/routes.py
··· 50 50 req_facet = request_event.get("facet") 51 51 req_name = request_event.get("name", "default") 52 52 req_env = request_event.get("env") or {} 53 - req_stream = req_env.get("STREAM_NAME") if req_env else None 53 + req_stream = req_env.get("SOL_STREAM") if req_env else None 54 54 return get_output_path( 55 55 day_dir, 56 56 req_name,
+10 -2
apps/entities/call.py
··· 52 52 53 53 @app.command("list") 54 54 def list_entities( 55 - facet: str = typer.Argument(help="Facet name."), 55 + facet: str | None = typer.Argument(None, help="Facet name (or set SOL_FACET)."), 56 56 day: str | None = typer.Option( 57 57 None, "--day", "-d", help="Day (YYYYMMDD) for detected entities." 58 58 ), 59 59 ) -> None: 60 60 """List entities for a facet.""" 61 + from think.utils import resolve_sol_facet 62 + 63 + facet = resolve_sol_facet(facet) 61 64 entities = load_entities(facet, day) 62 65 if not entities: 63 66 typer.echo("No entities found.") ··· 71 74 72 75 @app.command("detect") 73 76 def detect_entity( 74 - day: str = typer.Argument(help="Day (YYYYMMDD)."), 75 77 facet: str = typer.Argument(help="Facet name."), 76 78 type_: str = typer.Argument(metavar="TYPE", help="Entity type."), 77 79 entity: str = typer.Argument(help="Entity name or identifier."), 78 80 description: str = typer.Argument(help="Description."), 81 + day: str | None = typer.Option( 82 + None, "--day", "-d", help="Day (YYYYMMDD, or set SOL_DAY)." 83 + ), 79 84 ) -> None: 80 85 """Record a detected entity for a day in a facet.""" 86 + from think.utils import resolve_sol_day 87 + 88 + day = resolve_sol_day(day) 81 89 if not is_valid_entity_type(type_): 82 90 typer.echo(f"Error: Invalid entity type '{type_}'.", err=True) 83 91 raise typer.Exit(1)
+9 -6
apps/entities/muse/entities/SKILL.md
··· 6 6 # Entities CLI Skill 7 7 8 8 Use these commands to maintain facet-scoped entity memory from the terminal. 9 + 10 + **Environment defaults**: When `SOL_FACET` is set, commands that take a FACET argument will use it automatically. Same for `SOL_DAY` where DAY is accepted. 11 + 9 12 Common pattern: 10 13 11 14 ```bash ··· 24 27 ## list 25 28 26 29 ```bash 27 - sol call entities list FACET [-d DAY] 30 + sol call entities list [FACET] [-d DAY] 28 31 ``` 29 32 30 33 List entities for a facet. 31 34 32 - - `FACET`: required facet name. 35 + - `FACET`: facet name (default: `SOL_FACET` env). 33 36 - `-d, --day`: optional day (`YYYYMMDD`). 34 37 35 38 Behavior notes: ··· 47 50 ## detect 48 51 49 52 ```bash 50 - sol call entities detect DAY FACET TYPE ENTITY DESCRIPTION 53 + sol call entities detect FACET TYPE ENTITY DESCRIPTION [-d DAY] 51 54 ``` 52 55 53 56 Record a detected entity for a day. 54 57 55 - - `DAY`: required day in `YYYYMMDD`. 56 - - `FACET`: required facet name. 58 + - `FACET`: required facet name (positional argument). 57 59 - `TYPE`: entity type (alphanumeric + spaces, minimum 3 chars). 58 60 - `ENTITY`: entity id, full name, or alias. 59 61 - `DESCRIPTION`: day-scoped description. 62 + - `-d, --day`: day in `YYYYMMDD` (default: `SOL_DAY` env). 60 63 61 64 Behavior notes: 62 65 ··· 67 70 Example: 68 71 69 72 ```bash 70 - sol call entities detect 20260115 work "Person" "Alicia Chen" "Led architecture review" 73 + sol call entities detect work "Person" "Alicia Chen" "Led architecture review" -d 20260115 71 74 ``` 72 75 73 76 ## attach
+59 -3
apps/entities/tests/test_call.py
··· 81 81 [ 82 82 "entities", 83 83 "detect", 84 - "20240101", 85 84 "personal", 86 85 "Person", 87 86 "Alice", 88 87 "Met at conference", 88 + "--day", 89 + "20240101", 89 90 ], 90 91 ) 91 92 ··· 105 106 [ 106 107 "entities", 107 108 "detect", 108 - "20240101", 109 109 "personal", 110 110 "Person", 111 111 "Alice", 112 112 "Second", 113 + "--day", 114 + "20240101", 113 115 ], 114 116 ) 115 117 ··· 124 126 [ 125 127 "entities", 126 128 "detect", 127 - "20240101", 128 129 "personal", 129 130 "AB", 130 131 "Alice", 131 132 "Met at conference", 133 + "--day", 134 + "20240101", 132 135 ], 133 136 ) 134 137 ··· 386 389 387 390 assert result.exit_code == 1 388 391 assert "not found" in result.output 392 + 393 + 394 + class TestSolEnvResolution: 395 + """Tests for SOL_* env var resolution in entities commands.""" 396 + 397 + def test_list_from_sol_facet(self, entity_env, monkeypatch): 398 + """list with SOL_FACET env instead of positional arg works.""" 399 + entity_env( 400 + attached=[ 401 + { 402 + "type": "Person", 403 + "name": "Alice", 404 + "description": "Friend", 405 + "attached_at": 1000, 406 + "updated_at": 1000, 407 + } 408 + ] 409 + ) 410 + monkeypatch.setenv("SOL_FACET", "personal") 411 + result = runner.invoke(call_app, ["entities", "list"]) 412 + assert result.exit_code == 0 413 + assert "Alice" in result.output 414 + 415 + def test_detect_from_sol_day(self, entity_env, monkeypatch): 416 + """detect with SOL_DAY env instead of --day option works.""" 417 + entity_env() 418 + monkeypatch.setenv("SOL_DAY", "20240101") 419 + result = runner.invoke( 420 + call_app, 421 + ["entities", "detect", "personal", "Person", "Bob", "Met at party"], 422 + ) 423 + assert result.exit_code == 0 424 + assert "detected" in result.output 425 + 426 + def test_detect_arg_overrides_sol_day(self, entity_env, monkeypatch): 427 + """detect with explicit --day works even with SOL_DAY set.""" 428 + entity_env() 429 + monkeypatch.setenv("SOL_DAY", "19990101") 430 + result = runner.invoke( 431 + call_app, 432 + [ 433 + "entities", 434 + "detect", 435 + "personal", 436 + "Person", 437 + "Charlie", 438 + "Met at office", 439 + "--day", 440 + "20240101", 441 + ], 442 + ) 443 + assert result.exit_code == 0 444 + assert "detected" in result.output
+32 -11
apps/todos/call.py
··· 27 27 28 28 @app.command("list") 29 29 def list_todos( 30 - day: str = typer.Argument(help="Journal day in YYYYMMDD format."), 30 + day: str | None = typer.Argument( 31 + None, help="Journal day in YYYYMMDD format (or set SOL_DAY)." 32 + ), 31 33 facet: str | None = typer.Option( 32 34 None, "--facet", "-f", help="Facet name. Omit to show all facets." 33 35 ), ··· 36 38 ), 37 39 ) -> None: 38 40 """Show the todo checklist for a day (or date range).""" 39 - from think.utils import get_journal 41 + from think.utils import get_journal, resolve_sol_day 40 42 41 43 get_journal() 44 + day = resolve_sol_day(day) 42 45 43 46 if to is not None and to < day: 44 47 typer.echo(f"Error: --to ({to}) must not be before day ({day})", err=True) ··· 85 88 86 89 @app.command("add") 87 90 def add_todo( 88 - day: str = typer.Argument(help="Journal day in YYYYMMDD format."), 89 91 text: str = typer.Argument(help="Todo item text."), 90 - facet: str = typer.Option(..., "--facet", "-f", help="Facet name."), 92 + day: str | None = typer.Option( 93 + None, "--day", "-d", help="Journal day in YYYYMMDD format (or set SOL_DAY)." 94 + ), 95 + facet: str | None = typer.Option( 96 + None, "--facet", "-f", help="Facet name (or set SOL_FACET)." 97 + ), 91 98 ) -> None: 92 99 """Add a new todo item.""" 93 100 from datetime import datetime 94 101 95 - from think.utils import get_journal 102 + from think.utils import get_journal, resolve_sol_day, resolve_sol_facet 96 103 97 104 get_journal() 105 + day = resolve_sol_day(day) 106 + facet = resolve_sol_facet(facet) 98 107 99 108 # Reject past dates 100 109 try: ··· 119 128 120 129 @app.command("done") 121 130 def done_todo( 122 - day: str = typer.Argument(help="Journal day in YYYYMMDD format."), 123 131 line_number: int = typer.Argument(help="1-based line number of the todo."), 124 - facet: str = typer.Option(..., "--facet", "-f", help="Facet name."), 132 + day: str | None = typer.Option( 133 + None, "--day", "-d", help="Journal day in YYYYMMDD format (or set SOL_DAY)." 134 + ), 135 + facet: str | None = typer.Option( 136 + None, "--facet", "-f", help="Facet name (or set SOL_FACET)." 137 + ), 125 138 ) -> None: 126 139 """Mark a todo item as done.""" 127 - from think.utils import get_journal 140 + from think.utils import get_journal, resolve_sol_day, resolve_sol_facet 128 141 129 142 get_journal() 143 + day = resolve_sol_day(day) 144 + facet = resolve_sol_facet(facet) 130 145 131 146 try: 132 147 checklist = todo.TodoChecklist.load(day, facet) ··· 142 157 143 158 @app.command("cancel") 144 159 def cancel_todo( 145 - day: str = typer.Argument(help="Journal day in YYYYMMDD format."), 146 160 line_number: int = typer.Argument(help="1-based line number of the todo."), 147 - facet: str = typer.Option(..., "--facet", "-f", help="Facet name."), 161 + day: str | None = typer.Option( 162 + None, "--day", "-d", help="Journal day in YYYYMMDD format (or set SOL_DAY)." 163 + ), 164 + facet: str | None = typer.Option( 165 + None, "--facet", "-f", help="Facet name (or set SOL_FACET)." 166 + ), 148 167 ) -> None: 149 168 """Cancel a todo item.""" 150 - from think.utils import get_journal 169 + from think.utils import get_journal, resolve_sol_day, resolve_sol_facet 151 170 152 171 get_journal() 172 + day = resolve_sol_day(day) 173 + facet = resolve_sol_facet(facet) 153 174 154 175 try: 155 176 checklist = todo.TodoChecklist.load(day, facet)
+7 -7
apps/todos/muse/todo.md
··· 30 30 31 31 ## Tooling 32 32 33 - ### Todo Commands (require facet parameter) 34 - - `sol call todos list $day_YYYYMMDD -f FACET` – inspect the current numbered checklist 35 - - `sol call todos add $day_YYYYMMDD TEXT -f FACET` – append a new unchecked line (line number is auto-calculated) 36 - - `sol call todos done $day_YYYYMMDD LINE_NUMBER -f FACET` – mark an entry complete 37 - - `sol call todos upcoming -l LIMIT -f FACET` – view upcoming todos to avoid duplicates 33 + ### Todo Commands (SOL_DAY and SOL_FACET are set in your environment) 34 + - `sol call todos list` – inspect the current numbered checklist 35 + - `sol call todos add TEXT` – append a new unchecked line (line number is auto-calculated) 36 + - `sol call todos done LINE_NUMBER` – mark an entry complete 37 + - `sol call todos upcoming` – view upcoming todos to avoid duplicates 38 38 39 39 ### Transcript Commands (for deeper investigation) 40 - - `sol call transcripts read $day_YYYYMMDD --segment SEGMENT_KEY --full` – read full transcript for a specific segment (segment keys from this activity: $activity_segments) 41 - - `sol call journal search QUERY -d $day_YYYYMMDD` – cross-reference journal content 40 + - `sol call transcripts read --segment SEGMENT_KEY --full` – read full transcript for a specific segment (segment keys from this activity: $activity_segments) 41 + - `sol call journal search QUERY` – cross-reference journal content 42 42 43 43 **Query syntax**: Terms are AND'd by default; use `OR` for alternatives, quote phrases for exact matches, append `*` for prefix matching. 44 44
+21 -18
apps/todos/muse/todos/SKILL.md
··· 6 6 # Todos CLI Skill 7 7 8 8 Use these commands to manage checklist entries from the terminal. 9 + 10 + **Environment defaults**: When `SOL_DAY` is set, commands that take a DAY argument will use it automatically. Same for `SOL_FACET` where FACET is required. 11 + 9 12 Common pattern: 10 13 11 14 ```bash ··· 15 18 ## list 16 19 17 20 ```bash 18 - sol call todos list DAY [-f FACET] [--to DAY] 21 + sol call todos list [DAY] [-f FACET] [--to DAY] 19 22 ``` 20 23 21 24 Show checklist entries for one day or an inclusive day range. 22 25 23 - - `DAY`: required day in `YYYYMMDD`. 26 + - `DAY`: day in `YYYYMMDD` (default: `SOL_DAY` env). 24 27 - `-f, --facet`: optional facet filter. Omit to show all facets. 25 28 - `--to`: optional inclusive range end day in `YYYYMMDD`. 26 29 ··· 40 43 ## add 41 44 42 45 ```bash 43 - sol call todos add DAY TEXT -f FACET 46 + sol call todos add TEXT [-d DAY] [-f FACET] 44 47 ``` 45 48 46 49 Add a new todo item. 47 50 48 - - `DAY`: required day in `YYYYMMDD`; must be today or in the future. 49 - - `TEXT`: todo text. 50 - - `-f, --facet`: required facet name. 51 + - `TEXT`: todo text (positional argument). 52 + - `-d, --day`: day in `YYYYMMDD` (default: `SOL_DAY` env); must be today or in the future. 53 + - `-f, --facet`: facet name (default: `SOL_FACET` env). 51 54 52 55 Behavior notes: 53 56 ··· 58 61 Examples: 59 62 60 63 ```bash 61 - sol call todos add 20260115 "Draft Q1 plan" -f work 62 - sol call todos add 20260115 "Team sync prep (14:30)" -f work 64 + sol call todos add "Draft Q1 plan" -d 20260115 -f work 65 + sol call todos add "Team sync prep (14:30)" -d 20260115 -f work 63 66 ``` 64 67 65 68 ## done 66 69 67 70 ```bash 68 - sol call todos done DAY LINE_NUMBER -f FACET 71 + sol call todos done LINE_NUMBER [-d DAY] [-f FACET] 69 72 ``` 70 73 71 74 Mark a todo as complete. 72 75 73 - - `DAY`: required day in `YYYYMMDD`. 74 - - `LINE_NUMBER`: 1-based line number from `list` output. 75 - - `-f, --facet`: required facet name. 76 + - `LINE_NUMBER`: 1-based line number from `list` output (positional argument). 77 + - `-d, --day`: day in `YYYYMMDD` (default: `SOL_DAY` env). 78 + - `-f, --facet`: facet name (default: `SOL_FACET` env). 76 79 77 80 Example: 78 81 79 82 ```bash 80 - sol call todos done 20260115 2 -f work 83 + sol call todos done 2 -d 20260115 -f work 81 84 ``` 82 85 83 86 ## cancel 84 87 85 88 ```bash 86 - sol call todos cancel DAY LINE_NUMBER -f FACET 89 + sol call todos cancel LINE_NUMBER [-d DAY] [-f FACET] 87 90 ``` 88 91 89 92 Cancel (soft-delete) a todo. 90 93 91 - - `DAY`: required day in `YYYYMMDD`. 92 - - `LINE_NUMBER`: 1-based line number from `list` output. 93 - - `-f, --facet`: required facet name. 94 + - `LINE_NUMBER`: 1-based line number from `list` output (positional argument). 95 + - `-d, --day`: day in `YYYYMMDD` (default: `SOL_DAY` env). 96 + - `-f, --facet`: facet name (default: `SOL_FACET` env). 94 97 95 98 Behavior notes: 96 99 ··· 99 102 Example: 100 103 101 104 ```bash 102 - sol call todos cancel 20260115 4 -f work 105 + sol call todos cancel 4 -d 20260115 -f work 103 106 ``` 104 107 105 108 ## upcoming
+51 -8
apps/todos/tests/test_call.py
··· 64 64 todo_env([], day="29991231") 65 65 result = runner.invoke( 66 66 call_app, 67 - ["todos", "add", "29991231", "Ship feature", "--facet", "personal"], 67 + [ 68 + "todos", 69 + "add", 70 + "Ship feature", 71 + "--day", 72 + "29991231", 73 + "--facet", 74 + "personal", 75 + ], 68 76 ) 69 77 assert result.exit_code == 0 70 78 assert "Ship feature" in result.output ··· 73 81 """Add appends after existing items.""" 74 82 todo_env([{"text": "First"}], day="29991231") 75 83 result = runner.invoke( 76 - call_app, ["todos", "add", "29991231", "Second", "--facet", "personal"] 84 + call_app, 85 + ["todos", "add", "Second", "--day", "29991231", "--facet", "personal"], 77 86 ) 78 87 assert result.exit_code == 0 79 88 assert "First" in result.output ··· 83 92 """Adding to a past date fails.""" 84 93 todo_env([], day="20200101") 85 94 result = runner.invoke( 86 - call_app, ["todos", "add", "20200101", "Nope", "--facet", "personal"] 95 + call_app, 96 + ["todos", "add", "Nope", "--day", "20200101", "--facet", "personal"], 87 97 ) 88 98 assert result.exit_code == 1 89 99 ··· 91 101 """Adding empty text fails.""" 92 102 todo_env([], day="29991231") 93 103 result = runner.invoke( 94 - call_app, ["todos", "add", "29991231", " ", "--facet", "personal"] 104 + call_app, 105 + ["todos", "add", " ", "--day", "29991231", "--facet", "personal"], 95 106 ) 96 107 assert result.exit_code == 1 97 108 ··· 103 114 """Mark a todo as done.""" 104 115 todo_env([{"text": "Buy milk"}], day="20240101") 105 116 result = runner.invoke( 106 - call_app, ["todos", "done", "20240101", "1", "--facet", "personal"] 117 + call_app, ["todos", "done", "1", "--day", "20240101", "--facet", "personal"] 107 118 ) 108 119 assert result.exit_code == 0 109 120 assert "[x]" in result.output ··· 112 123 """Invalid line number fails.""" 113 124 todo_env([{"text": "Only one"}], day="20240101") 114 125 result = runner.invoke( 115 - call_app, ["todos", "done", "20240101", "5", "--facet", "personal"] 126 + call_app, ["todos", "done", "5", "--day", "20240101", "--facet", "personal"] 116 127 ) 117 128 assert result.exit_code == 1 118 129 ··· 124 135 """Cancel a todo.""" 125 136 todo_env([{"text": "Buy milk"}], day="20240101") 126 137 result = runner.invoke( 127 - call_app, ["todos", "cancel", "20240101", "1", "--facet", "personal"] 138 + call_app, 139 + ["todos", "cancel", "1", "--day", "20240101", "--facet", "personal"], 128 140 ) 129 141 assert result.exit_code == 0 130 142 assert "cancelled" in result.output ··· 133 145 """Invalid line number fails.""" 134 146 todo_env([{"text": "Only one"}], day="20240101") 135 147 result = runner.invoke( 136 - call_app, ["todos", "cancel", "20240101", "5", "--facet", "personal"] 148 + call_app, 149 + ["todos", "cancel", "5", "--day", "20240101", "--facet", "personal"], 137 150 ) 138 151 assert result.exit_code == 1 139 152 ··· 161 174 result = runner.invoke(call_app, ["todos", "upcoming"]) 162 175 assert result.exit_code == 0 163 176 assert "No upcoming todos" in result.output 177 + 178 + 179 + class TestSolEnvResolution: 180 + """Tests for SOL_* env var resolution in todos commands.""" 181 + 182 + def test_list_from_sol_day(self, todo_env, monkeypatch): 183 + """list with SOL_DAY env and no day arg works.""" 184 + todo_env([{"text": "Env task"}], day="20240101") 185 + monkeypatch.setenv("SOL_DAY", "20240101") 186 + result = runner.invoke(call_app, ["todos", "list", "--facet", "personal"]) 187 + assert result.exit_code == 0 188 + assert "Env task" in result.output 189 + 190 + def test_add_from_sol_day_and_facet(self, todo_env, monkeypatch): 191 + """add with SOL_DAY + SOL_FACET env works.""" 192 + todo_env([], day="29991231") 193 + monkeypatch.setenv("SOL_DAY", "29991231") 194 + monkeypatch.setenv("SOL_FACET", "personal") 195 + result = runner.invoke(call_app, ["todos", "add", "Env todo"]) 196 + assert result.exit_code == 0 197 + assert "Env todo" in result.output 198 + 199 + def test_done_from_sol_day_and_facet(self, todo_env, monkeypatch): 200 + """done with SOL_DAY + SOL_FACET env works.""" 201 + todo_env([{"text": "Buy milk"}], day="20240101") 202 + monkeypatch.setenv("SOL_DAY", "20240101") 203 + monkeypatch.setenv("SOL_FACET", "personal") 204 + result = runner.invoke(call_app, ["todos", "done", "1"]) 205 + assert result.exit_code == 0 206 + assert "[x]" in result.output
+29 -6
apps/transcripts/call.py
··· 18 18 cluster_scan, 19 19 cluster_segments, 20 20 ) 21 - from think.utils import day_dirs, truncated_echo 21 + from think.utils import ( 22 + day_dirs, 23 + get_sol_stream, 24 + resolve_sol_day, 25 + resolve_sol_segment, 26 + truncated_echo, 27 + ) 22 28 23 29 app = typer.Typer(help="Transcript browsing.") 24 30 25 31 26 32 @app.command("scan") 27 - def scan(day: str = typer.Argument(help="Day (YYYYMMDD).")) -> None: 33 + def scan( 34 + day: str | None = typer.Argument( 35 + default=None, help="Day YYYYMMDD (default: SOL_DAY env)." 36 + ), 37 + ) -> None: 28 38 """List transcript coverage ranges for a day.""" 39 + day = resolve_sol_day(day) 29 40 audio_ranges, screen_ranges = cluster_scan(day) 30 41 31 42 typer.echo("Audio:") ··· 44 55 45 56 46 57 @app.command("segments") 47 - def segments(day: str = typer.Argument(help="Day (YYYYMMDD).")) -> None: 58 + def segments( 59 + day: str | None = typer.Argument( 60 + default=None, help="Day YYYYMMDD (default: SOL_DAY env)." 61 + ), 62 + ) -> None: 48 63 """List recording segments for a day.""" 64 + day = resolve_sol_day(day) 49 65 segment_list = cluster_segments(day) 50 66 if not segment_list: 51 67 typer.echo("No segments.") ··· 61 77 62 78 @app.command("read") 63 79 def read( 64 - day: str = typer.Argument(help="Day (YYYYMMDD)."), 80 + day: str | None = typer.Argument( 81 + default=None, help="Day YYYYMMDD (default: SOL_DAY env)." 82 + ), 65 83 start: str | None = typer.Option(None, "--start", help="Start time (HHMMSS)."), 66 84 length: int | None = typer.Option(None, "--length", help="Length in minutes."), 67 85 segment: str | None = typer.Option( 68 - None, "--segment", help="Segment key (HHMMSS_LEN)." 86 + None, "--segment", help="Segment key (HHMMSS_LEN, default: SOL_SEGMENT env)." 87 + ), 88 + stream: str | None = typer.Option( 89 + None, "--stream", help="Stream name (default: SOL_STREAM env)." 69 90 ), 70 - stream: str | None = typer.Option(None, "--stream", help="Stream name."), 71 91 full: bool = typer.Option( 72 92 False, "--full", help="Include audio, screen, and agents." 73 93 ), ··· 80 100 ), 81 101 ) -> None: 82 102 """Read transcript content for a day, segment, or time range.""" 103 + day = resolve_sol_day(day) 104 + segment = resolve_sol_segment(segment) 105 + stream = stream or get_sol_stream() 83 106 if full and raw: 84 107 typer.echo("Error: Cannot use --full and --raw together.", err=True) 85 108 raise typer.Exit(1)
+9 -6
apps/transcripts/muse/transcripts/SKILL.md
··· 6 6 # Transcripts CLI Skill 7 7 8 8 Use these commands to inspect transcript availability and content from the terminal. 9 + 10 + **Environment defaults**: When `SOL_DAY` is set, commands that take a DAY argument will use it automatically. `SOL_SEGMENT` and `SOL_STREAM` provide defaults for `--segment` and `--stream` options. 11 + 9 12 Common pattern: 10 13 11 14 ```bash ··· 17 20 ## scan 18 21 19 22 ```bash 20 - sol call transcripts scan DAY 23 + sol call transcripts scan [DAY] 21 24 ``` 22 25 23 26 Show audio and screen coverage ranges. 24 27 25 - - `DAY`: required day in `YYYYMMDD`. 28 + - `DAY`: day in `YYYYMMDD` (default: `SOL_DAY` env). 26 29 27 30 Use this first to confirm what recording windows exist before running detailed reads. 28 31 ··· 35 38 ## segments 36 39 37 40 ```bash 38 - sol call transcripts segments DAY 41 + sol call transcripts segments [DAY] 39 42 ``` 40 43 41 44 List recording segments and their source types. 42 45 43 - - `DAY`: required day in `YYYYMMDD`. 46 + - `DAY`: day in `YYYYMMDD` (default: `SOL_DAY` env). 44 47 45 48 Behavior notes: 46 49 ··· 58 61 ## read 59 62 60 63 ```bash 61 - sol call transcripts read DAY [--start HHMMSS --length MINUTES] [--segment KEY] [--full] [--raw] [--audio] [--screen] [--agents] 64 + sol call transcripts read [DAY] [--start HHMMSS --length MINUTES] [--segment KEY] [--stream NAME] [--full] [--raw] [--audio] [--screen] [--agents] 62 65 ``` 63 66 64 67 Read transcript content for a day, time range, or segment. 65 68 66 - - `DAY`: required day in `YYYYMMDD`. 69 + - `DAY`: day in `YYYYMMDD` (default: `SOL_DAY` env). 67 70 68 71 Read modes (mutually exclusive): 69 72
+24
apps/transcripts/tests/test_call.py
··· 103 103 result = runner.invoke(call_app, ["transcripts", "stats", "209901"]) 104 104 assert result.exit_code == 0 105 105 assert "No data" in result.output 106 + 107 + 108 + class TestSolEnvResolution: 109 + """Tests for SOL_* env var resolution in transcripts commands.""" 110 + 111 + def test_scan_from_sol_day(self, monkeypatch): 112 + """scan with SOL_DAY env and no arg works.""" 113 + monkeypatch.setenv("SOL_DAY", "20240101") 114 + result = runner.invoke(call_app, ["transcripts", "scan"]) 115 + assert result.exit_code == 0 116 + assert "Audio:" in result.output 117 + 118 + def test_read_from_sol_day(self, monkeypatch): 119 + """read with SOL_DAY env and no arg works.""" 120 + monkeypatch.setenv("SOL_DAY", "20240101") 121 + result = runner.invoke(call_app, ["transcripts", "read"]) 122 + assert result.exit_code == 0 123 + 124 + def test_read_from_sol_day_and_segment(self, monkeypatch): 125 + """read with SOL_DAY + SOL_SEGMENT env works.""" 126 + monkeypatch.setenv("SOL_DAY", "20240101") 127 + monkeypatch.setenv("SOL_SEGMENT", "123456_300") 128 + result = runner.invoke(call_app, ["transcripts", "read"]) 129 + assert result.exit_code == 0
+54 -6
muse/journal/SKILL.md
··· 6 6 # Journal CLI Skill 7 7 8 8 Use these commands to explore journal content from the terminal. 9 + 10 + **Environment defaults**: When `SOL_DAY` is set, commands that take a DAY argument will use it automatically. Same for `SOL_SEGMENT`. 11 + 9 12 Common pattern: 10 13 11 14 ```bash ··· 50 53 ## events 51 54 52 55 ```bash 53 - sol call journal events DAY [-f FACET] 56 + sol call journal events [DAY] [-f FACET] 54 57 ``` 55 58 56 59 List structured events for a day. 57 60 58 - - `DAY`: required day in `YYYYMMDD`. 61 + - `DAY`: day in `YYYYMMDD` (default: `SOL_DAY` env). 59 62 - `-f, --facet`: optional facet filter. 60 63 61 64 Use this when you need full event records (titles, summaries, times, participants), not just search snippets. ··· 85 88 sol call journal facet work 86 89 ``` 87 90 91 + ## topics 92 + 93 + ```bash 94 + sol call journal topics [DAY] [-s SEGMENT] 95 + ``` 96 + 97 + List available agent outputs for a day. 98 + 99 + - `DAY`: day in `YYYYMMDD` (default: `SOL_DAY` env). 100 + - `-s, --segment`: optional segment key (default: `SOL_SEGMENT` env). 101 + 102 + Without `--segment`, lists daily agent outputs and per-segment outputs. With `--segment`, lists only that segment's outputs. 103 + 104 + Example: 105 + 106 + ```bash 107 + sol call journal topics 20260115 108 + sol call journal topics -s 091500_300 109 + ``` 110 + 111 + ## read 112 + 113 + ```bash 114 + sol call journal read TOPIC [-d DAY] [-s SEGMENT] [--max BYTES] 115 + ``` 116 + 117 + Read full content of an agent output. 118 + 119 + - `TOPIC`: topic name, e.g. `flow`, `meetings`, `activity` (positional argument). 120 + - `-d, --day`: day in `YYYYMMDD` (default: `SOL_DAY` env). 121 + - `-s, --segment`: optional segment key (default: `SOL_SEGMENT` env). 122 + - `--max`: max output bytes (default `16384`, `0` for unlimited). 123 + 124 + Without `--segment`, reads from the daily agents directory. With `--segment`, reads from that segment's agents directory. 125 + 126 + Examples: 127 + 128 + ```bash 129 + sol call journal read flow -d 20260115 130 + sol call journal read meetings 131 + sol call journal read activity -s 091500_300 132 + ``` 133 + 88 134 ## news 89 135 90 136 ```bash 91 - sol call journal news NAME [-d DAY] [-n LIMIT] [--cursor CURSOR] 137 + sol call journal news NAME [-d DAY] [-n LIMIT] [--cursor CURSOR] [-w] 92 138 ``` 93 139 94 - Read facet news entries (read-only feed in CLI). 140 + Read or write facet news entries. 95 141 96 142 - `NAME`: facet name. 97 - - `-d, --day`: optional specific day (`YYYYMMDD`). 143 + - `-d, --day`: optional specific day (`YYYYMMDD`, default: `SOL_DAY` env for write mode). 98 144 - `-n, --limit`: max days to return (default `5`). 99 145 - `--cursor`: optional pagination cursor (typically a `YYYYMMDD` cutoff for older entries). 146 + - `-w, --write`: write mode — reads markdown from stdin and saves as news for the given day. 100 147 101 148 Behavior notes: 102 149 103 - - CLI `news` reads content only. It does not provide write behavior. 150 + - Without `--write`: reads and displays existing news entries. 151 + - With `--write`: requires `--day` (or `SOL_DAY` env), reads markdown content from stdin, saves to facet news directory. 104 152 105 153 Examples: 106 154
+1 -1
observe/sense.py
··· 311 311 # Build environment with segment and remote context for handlers 312 312 env = os.environ.copy() 313 313 if segment: 314 - env["SEGMENT_KEY"] = segment 314 + env["SOL_SEGMENT"] = segment 315 315 if remote: 316 316 env["REMOTE_NAME"] = remote 317 317 if meta:
+2 -2
tests/test_app_agents.py
··· 217 217 assert result is not None 218 218 219 219 def test_uses_env_stream_name(self, fixture_journal): 220 - """STREAM_NAME from env is passed through to get_output_path.""" 220 + """SOL_STREAM from env is passed through to get_output_path.""" 221 221 event = { 222 222 "day": "20260214", 223 223 "name": "default", 224 - "env": {"STREAM_NAME": "mystream"}, 224 + "env": {"SOL_STREAM": "mystream"}, 225 225 } 226 226 result = _resolve_output_path(event, "tests/fixtures/journal") 227 227 assert result is not None
+115 -5
tests/test_call.py
··· 3 3 4 4 """Tests for think/call.py CLI dispatcher and app discovery.""" 5 5 6 + import typer 6 7 from typer.testing import CliRunner 7 8 8 9 from think.call import call_app 10 + from think.utils import resolve_sol_day, resolve_sol_facet, resolve_sol_segment 9 11 10 12 runner = CliRunner() 11 13 ··· 113 115 114 116 def test_journal_read(self): 115 117 """Read command returns full agent output content.""" 116 - result = runner.invoke(call_app, ["journal", "read", "20240101", "flow"]) 118 + result = runner.invoke( 119 + call_app, ["journal", "read", "flow", "--day", "20240101"] 120 + ) 117 121 assert result.exit_code == 0 118 122 assert len(result.output.strip()) > 0 119 123 ··· 121 125 """Read command truncates output when --max is exceeded.""" 122 126 # flow.md is ~422 bytes; --max 50 should truncate 123 127 result = runner.invoke( 124 - call_app, ["journal", "read", "20240101", "flow", "--max", "50"] 128 + call_app, ["journal", "read", "flow", "--day", "20240101", "--max", "50"] 125 129 ) 126 130 assert result.exit_code == 0 127 131 # Output should be much shorter than the full file ··· 130 134 def test_journal_read_max_zero_unlimited(self): 131 135 """Read command with --max 0 returns full content.""" 132 136 result = runner.invoke( 133 - call_app, ["journal", "read", "20240101", "flow", "--max", "0"] 137 + call_app, ["journal", "read", "flow", "--day", "20240101", "--max", "0"] 134 138 ) 135 139 assert result.exit_code == 0 136 140 assert len(result.output.strip()) > 100 137 141 138 142 def test_journal_read_not_found(self): 139 143 """Read command reports missing topic.""" 140 - result = runner.invoke(call_app, ["journal", "read", "20240101", "nonexistent"]) 144 + result = runner.invoke( 145 + call_app, ["journal", "read", "nonexistent", "--day", "20240101"] 146 + ) 141 147 assert result.exit_code == 1 142 148 assert "not found" in result.output.lower() 143 149 ··· 181 187 input="content", 182 188 ) 183 189 assert result.exit_code == 1 184 - assert "--day" in result.output 190 + assert "day is required" in result.output 191 + 192 + 193 + class TestResolveHelpers: 194 + """Unit tests for SOL_* resolve helpers in think/utils.py.""" 195 + 196 + def test_resolve_sol_day_from_env(self, monkeypatch): 197 + """resolve_sol_day(None) with SOL_DAY set returns env value.""" 198 + monkeypatch.setenv("SOL_DAY", "20240101") 199 + assert resolve_sol_day(None) == "20240101" 200 + 201 + def test_resolve_sol_day_arg_wins(self, monkeypatch): 202 + """resolve_sol_day with explicit arg ignores env.""" 203 + monkeypatch.setenv("SOL_DAY", "20240101") 204 + assert resolve_sol_day("20260115") == "20260115" 205 + 206 + def test_resolve_sol_day_missing_exits(self, monkeypatch): 207 + """resolve_sol_day(None) with no env raises SystemExit.""" 208 + monkeypatch.delenv("SOL_DAY", raising=False) 209 + try: 210 + resolve_sol_day(None) 211 + assert False, "Expected typer.Exit" 212 + except (typer.Exit, SystemExit): 213 + pass 214 + 215 + def test_resolve_sol_facet_from_env(self, monkeypatch): 216 + """resolve_sol_facet(None) with SOL_FACET set returns env value.""" 217 + monkeypatch.setenv("SOL_FACET", "work") 218 + assert resolve_sol_facet(None) == "work" 219 + 220 + def test_resolve_sol_facet_arg_wins(self, monkeypatch): 221 + """resolve_sol_facet with explicit arg ignores env.""" 222 + monkeypatch.setenv("SOL_FACET", "work") 223 + assert resolve_sol_facet("personal") == "personal" 224 + 225 + def test_resolve_sol_facet_missing_exits(self, monkeypatch): 226 + """resolve_sol_facet(None) with no env raises SystemExit.""" 227 + monkeypatch.delenv("SOL_FACET", raising=False) 228 + try: 229 + resolve_sol_facet(None) 230 + assert False, "Expected typer.Exit" 231 + except (typer.Exit, SystemExit): 232 + pass 233 + 234 + def test_resolve_sol_segment_from_env(self, monkeypatch): 235 + """resolve_sol_segment(None) with SOL_SEGMENT returns env value.""" 236 + monkeypatch.setenv("SOL_SEGMENT", "123456_300") 237 + assert resolve_sol_segment(None) == "123456_300" 238 + 239 + def test_resolve_sol_segment_arg_wins(self, monkeypatch): 240 + """resolve_sol_segment with explicit arg ignores env.""" 241 + monkeypatch.setenv("SOL_SEGMENT", "123456_300") 242 + assert resolve_sol_segment("654321_600") == "654321_600" 243 + 244 + def test_resolve_sol_segment_missing_returns_none(self, monkeypatch): 245 + """resolve_sol_segment(None) with no env returns None.""" 246 + monkeypatch.delenv("SOL_SEGMENT", raising=False) 247 + assert resolve_sol_segment(None) is None 248 + 249 + 250 + class TestJournalSolEnv: 251 + """Tests for journal commands resolving SOL_* env vars.""" 252 + 253 + def test_events_from_sol_day(self, monkeypatch): 254 + """events with SOL_DAY env and no arg works.""" 255 + monkeypatch.setenv("SOL_DAY", "20240101") 256 + result = runner.invoke(call_app, ["journal", "events"]) 257 + assert result.exit_code == 0 258 + assert "Team standup" in result.output 259 + 260 + def test_events_arg_overrides_env(self, monkeypatch): 261 + """events with both env and arg — arg wins.""" 262 + monkeypatch.setenv("SOL_DAY", "19990101") 263 + result = runner.invoke(call_app, ["journal", "events", "20240101"]) 264 + assert result.exit_code == 0 265 + assert "Team standup" in result.output 266 + 267 + def test_events_no_day_exits(self, monkeypatch): 268 + """events with neither arg nor env exits with error.""" 269 + monkeypatch.delenv("SOL_DAY", raising=False) 270 + result = runner.invoke(call_app, ["journal", "events"]) 271 + assert result.exit_code != 0 272 + 273 + def test_topics_from_sol_day(self, monkeypatch): 274 + """topics with SOL_DAY env and no arg works.""" 275 + monkeypatch.setenv("SOL_DAY", "20240101") 276 + result = runner.invoke(call_app, ["journal", "topics"]) 277 + assert result.exit_code == 0 278 + assert "flow.md" in result.output 279 + 280 + def test_read_from_sol_day(self, monkeypatch): 281 + """read with SOL_DAY env and no --day works.""" 282 + monkeypatch.setenv("SOL_DAY", "20240101") 283 + result = runner.invoke(call_app, ["journal", "read", "flow"]) 284 + assert result.exit_code == 0 285 + assert len(result.output.strip()) > 0 286 + 287 + def test_read_arg_overrides_sol_day(self, monkeypatch): 288 + """read with explicit --day works even with SOL_DAY set.""" 289 + monkeypatch.setenv("SOL_DAY", "19990101") 290 + result = runner.invoke( 291 + call_app, ["journal", "read", "flow", "--day", "20240101"] 292 + ) 293 + assert result.exit_code == 0 294 + assert len(result.output.strip()) > 0
+1 -1
think/activities.py
··· 577 577 """ 578 578 from think.cluster import _find_segment_dir 579 579 580 - stream = os.environ.get("STREAM_NAME") 580 + stream = os.environ.get("SOL_STREAM") 581 581 seg_dir = _find_segment_dir(day, segment, stream) 582 582 if not seg_dir: 583 583 return None
+4 -4
think/agents.py
··· 290 290 """ 291 291 # Set segment key for token usage logging 292 292 if segment: 293 - os.environ["SEGMENT_KEY"] = segment 293 + os.environ["SOL_SEGMENT"] = segment 294 294 elif span: 295 - os.environ["SEGMENT_KEY"] = span[0] 295 + os.environ["SOL_SEGMENT"] = span[0] 296 296 297 297 # Convert sources config for clustering 298 298 cluster_sources: dict = {} ··· 309 309 cluster_sources[k] = source_is_enabled(v) 310 310 311 311 # Build transcript via clustering 312 - stream = os.environ.get("STREAM_NAME") 312 + stream = os.environ.get("SOL_STREAM") 313 313 if span: 314 314 return cluster_span(day, span, sources=cluster_sources, stream=stream) 315 315 elif segment: ··· 486 486 if output_path_override: 487 487 config["output_path"] = Path(output_path_override) 488 488 elif day: 489 - stream = os.environ.get("STREAM_NAME") 489 + stream = os.environ.get("SOL_STREAM") 490 490 day_dir = str(day_path(day)) 491 491 config["output_path"] = get_output_path( 492 492 day_dir,
+1 -1
think/cortex.py
··· 414 414 # Extract segment from env if set (flat merge puts env at top level) 415 415 env_config = original_request.get("env", {}) 416 416 segment = ( 417 - env_config.get("SEGMENT_KEY") 417 + env_config.get("SOL_SEGMENT") 418 418 if env_config 419 419 else None 420 420 )
+33 -11
think/dream.py
··· 431 431 request_config["output"] = config.get("output", "md") 432 432 if refresh: 433 433 request_config["refresh"] = True 434 + env: dict[str, str] = { 435 + "SOL_DAY": day, 436 + "SOL_FACET": facet_name, 437 + } 434 438 if segment: 435 439 request_config["segment"] = segment 436 - request_config["env"] = {"SEGMENT_KEY": segment} 440 + env["SOL_SEGMENT"] = segment 437 441 if stream: 438 - request_config["env"]["STREAM_NAME"] = stream 442 + env["SOL_STREAM"] = stream 443 + request_config["env"] = env 439 444 440 445 prompt = ( 441 446 "" ··· 497 502 request_config["output"] = config.get("output", "md") 498 503 if refresh: 499 504 request_config["refresh"] = True 505 + env: dict[str, str] = {"SOL_DAY": day} 500 506 if segment: 501 507 request_config["segment"] = segment 502 - request_config["env"] = {"SEGMENT_KEY": segment} 508 + env["SOL_SEGMENT"] = segment 503 509 if stream: 504 - request_config["env"]["STREAM_NAME"] = stream 510 + env["SOL_STREAM"] = stream 511 + request_config["env"] = env 505 512 506 513 prompt = ( 507 514 "" ··· 759 766 try: 760 767 logging.info(f"Spawning {name} for facet: {facet_name}") 761 768 request_config = {"facet": facet_name, "day": day} 769 + env: dict[str, str] = { 770 + "SOL_DAY": day, 771 + "SOL_FACET": facet_name, 772 + } 762 773 if segment: 763 774 request_config["segment"] = segment 764 - request_config["env"] = {"SEGMENT_KEY": segment} 775 + env["SOL_SEGMENT"] = segment 765 776 if stream: 766 - request_config["env"]["STREAM_NAME"] = stream 777 + env["SOL_STREAM"] = stream 778 + request_config["env"] = env 767 779 agent_id = _cortex_request_with_retry( 768 780 prompt=f"Processing facet '{facet_name}' for {day_formatted}: {input_summary}. Use get_facet('{facet_name}') to load context.", 769 781 name=name, ··· 790 802 else: 791 803 try: 792 804 request_config = {"day": day} 805 + env: dict[str, str] = {"SOL_DAY": day} 793 806 if segment: 794 807 request_config["segment"] = segment 795 - request_config["env"] = {"SEGMENT_KEY": segment} 808 + env["SOL_SEGMENT"] = segment 796 809 if stream: 797 - request_config["env"]["STREAM_NAME"] = stream 810 + env["SOL_STREAM"] = stream 811 + request_config["env"] = env 798 812 799 813 agent_id = _cortex_request_with_retry( 800 814 prompt=f"Running task for {day_formatted}: {input_summary}.", ··· 1091 1105 output_format=output_format, 1092 1106 ) 1093 1107 ), 1108 + "env": { 1109 + "SOL_DAY": day, 1110 + "SOL_FACET": facet, 1111 + "SOL_ACTIVITY": activity_id, 1112 + }, 1094 1113 } 1095 1114 if is_generate: 1096 1115 request_config["output"] = output_format ··· 1246 1265 is_generate = config["type"] == "generate" 1247 1266 1248 1267 try: 1249 - env: dict[str, str] = {"SEGMENT_KEY": segment} 1268 + env: dict[str, str] = { 1269 + "SOL_SEGMENT": segment, 1270 + "SOL_DAY": day, 1271 + } 1250 1272 if stream: 1251 - env["STREAM_NAME"] = stream 1273 + env["SOL_STREAM"] = stream 1252 1274 request_config: dict = { 1253 1275 "day": day, 1254 1276 "segment": segment, ··· 1381 1403 ) 1382 1404 parser.add_argument( 1383 1405 "--stream", 1384 - help="Stream name (e.g., 'archon', 'import.apple'). Passed to agents as STREAM_NAME env var.", 1406 + help="Stream name (e.g., 'archon', 'import.apple'). Passed to agents as SOL_STREAM env var.", 1385 1407 ) 1386 1408 parser.add_argument( 1387 1409 "--flush",
+2 -2
think/models.py
··· 530 530 If None, auto-detects from call stack. 531 531 segment : str, optional 532 532 Segment key (e.g., "143022_300") for attribution. 533 - If None, falls back to SEGMENT_KEY environment variable. 533 + If None, falls back to SOL_SEGMENT environment variable. 534 534 type : str, optional 535 535 Token entry type (e.g., "generate", "cogitate"). 536 536 """ ··· 637 637 } 638 638 639 639 # Add segment: prefer parameter, fallback to env (set by think/insight, observe handlers) 640 - segment_key = segment or os.getenv("SEGMENT_KEY") 640 + segment_key = segment or os.getenv("SOL_SEGMENT") 641 641 if segment_key: 642 642 token_data["segment"] = segment_key 643 643 if type:
+1 -1
think/muse_cli.py
··· 805 805 req_facet = request_event.get("facet") 806 806 req_name = request_event.get("name", "default") 807 807 req_env = request_event.get("env") or {} 808 - req_stream = req_env.get("STREAM_NAME") if req_env else None 808 + req_stream = req_env.get("SOL_STREAM") if req_env else None 809 809 day_dir = Path(journal_root) / req_day 810 810 out_path = get_output_path( 811 811 day_dir,
+30 -12
think/tools/call.py
··· 10 10 Mounted by ``think.call`` as ``sol call journal ...``. 11 11 """ 12 12 13 - import re 14 13 import sys 15 14 from pathlib import Path 16 15 ··· 20 19 from think.indexer.journal import get_events as get_events_impl 21 20 from think.indexer.journal import search_counts as search_counts_impl 22 21 from think.indexer.journal import search_journal as search_journal_impl 23 - from think.utils import get_journal, iter_segments, truncated_echo 22 + from think.utils import ( 23 + get_journal, 24 + iter_segments, 25 + resolve_sol_day, 26 + resolve_sol_segment, 27 + truncated_echo, 28 + ) 24 29 25 30 app = typer.Typer(help="Journal search and browsing.") 26 - 27 - SEGMENT_RE = re.compile(r"\d{6}_\d+") 28 31 29 32 30 33 @app.command() ··· 86 89 87 90 @app.command() 88 91 def events( 89 - day: str = typer.Argument(help="Day in YYYYMMDD format."), 92 + day: str | None = typer.Argument( 93 + default=None, help="Day YYYYMMDD (default: SOL_DAY env)." 94 + ), 90 95 facet: str | None = typer.Option(None, "--facet", "-f", help="Filter by facet."), 91 96 ) -> None: 92 97 """List events for a day.""" 98 + day = resolve_sol_day(day) 93 99 items = get_events_impl(day, facet) 94 100 if not items: 95 101 typer.echo("No events found.") ··· 151 157 ) -> None: 152 158 """Read or write facet news.""" 153 159 if write: 154 - if not day: 155 - typer.echo("Error: --day is required when writing news.", err=True) 156 - raise typer.Exit(1) 160 + day = resolve_sol_day(day) 157 161 158 162 # Read markdown from stdin 159 163 markdown = sys.stdin.read() ··· 185 189 186 190 @app.command() 187 191 def topics( 188 - day: str = typer.Argument(help="Day in YYYYMMDD format."), 192 + day: str | None = typer.Argument( 193 + default=None, help="Day YYYYMMDD (default: SOL_DAY env)." 194 + ), 189 195 segment: str | None = typer.Option( 190 - None, "--segment", "-s", help="Segment key (HHMMSS_LEN)." 196 + None, 197 + "--segment", 198 + "-s", 199 + help="Segment key (HHMMSS_LEN, default: SOL_SEGMENT env).", 191 200 ), 192 201 ) -> None: 193 202 """List available agent outputs for a day.""" 203 + day = resolve_sol_day(day) 204 + segment = resolve_sol_segment(segment) 194 205 journal = get_journal() 195 206 day_path = Path(journal) / day 196 207 ··· 256 267 257 268 @app.command() 258 269 def read( 259 - day: str = typer.Argument(help="Day in YYYYMMDD format."), 260 270 topic: str = typer.Argument(help="Topic name (e.g., flow, meetings, activity)."), 271 + day: str | None = typer.Option( 272 + None, "--day", "-d", help="Day YYYYMMDD (default: SOL_DAY env)." 273 + ), 261 274 segment: str | None = typer.Option( 262 - None, "--segment", "-s", help="Segment key (HHMMSS_LEN)." 275 + None, 276 + "--segment", 277 + "-s", 278 + help="Segment key (HHMMSS_LEN, default: SOL_SEGMENT env).", 263 279 ), 264 280 max_bytes: int = typer.Option( 265 281 16384, "--max", help="Max output bytes (0 = unlimited)." 266 282 ), 267 283 ) -> None: 268 284 """Read full content of an agent output.""" 285 + day = resolve_sol_day(day) 286 + segment = resolve_sol_segment(segment) 269 287 journal = get_journal() 270 288 day_path = Path(journal) / day 271 289
+77
think/utils.py
··· 779 779 780 780 781 781 # ============================================================================= 782 + # SOL_* Environment Variable Helpers 783 + # ============================================================================= 784 + 785 + 786 + def get_sol_day() -> str | None: 787 + """Read SOL_DAY from the environment.""" 788 + return os.environ.get("SOL_DAY") or None 789 + 790 + 791 + def get_sol_facet() -> str | None: 792 + """Read SOL_FACET from the environment.""" 793 + return os.environ.get("SOL_FACET") or None 794 + 795 + 796 + def get_sol_segment() -> str | None: 797 + """Read SOL_SEGMENT from the environment.""" 798 + return os.environ.get("SOL_SEGMENT") or None 799 + 800 + 801 + def get_sol_stream() -> str | None: 802 + """Read SOL_STREAM from the environment.""" 803 + return os.environ.get("SOL_STREAM") or None 804 + 805 + 806 + def get_sol_activity() -> str | None: 807 + """Read SOL_ACTIVITY from the environment.""" 808 + return os.environ.get("SOL_ACTIVITY") or None 809 + 810 + 811 + def resolve_sol_day(arg: str | None) -> str: 812 + """Return *arg* if provided, else SOL_DAY from env, else exit with error. 813 + 814 + Intended for CLI commands where ``day`` is required but can be supplied 815 + via the SOL_DAY environment variable as a convenience. 816 + """ 817 + if arg: 818 + return arg 819 + env = get_sol_day() 820 + if env: 821 + return env 822 + import typer 823 + 824 + typer.echo("Error: day is required (pass as argument or set SOL_DAY).", err=True) 825 + raise typer.Exit(1) 826 + 827 + 828 + def resolve_sol_facet(arg: str | None) -> str: 829 + """Return *arg* if provided, else SOL_FACET from env, else exit with error. 830 + 831 + Intended for CLI commands where ``facet`` is required but can be supplied 832 + via the SOL_FACET environment variable as a convenience. 833 + """ 834 + if arg: 835 + return arg 836 + env = get_sol_facet() 837 + if env: 838 + return env 839 + import typer 840 + 841 + typer.echo( 842 + "Error: facet is required (pass as argument or set SOL_FACET).", err=True 843 + ) 844 + raise typer.Exit(1) 845 + 846 + 847 + def resolve_sol_segment(arg: str | None) -> str | None: 848 + """Return *arg* if provided, else SOL_SEGMENT from env, else None. 849 + 850 + Unlike :func:`resolve_sol_day` this does **not** error when missing 851 + because segment is typically optional. 852 + """ 853 + if arg: 854 + return arg 855 + return get_sol_segment() 856 + 857 + 858 + # ============================================================================= 782 859 # Service Port Discovery 783 860 # ============================================================================= 784 861