@contextmanager
def Live(start, **kw):
print(start)
def update(s, refresh=False): clear_output(True);print(s)
yield NS(update=update)Core
Imports
CodeBlock.__rich_console__
def __rich_console__(
console, options
):
Call self as a function.
Chat
def Chat(
arg:VAR_POSITIONAL, kw:VAR_KEYWORD
):
Lazy load lisette to make ssage more responsive
Jupyter does work with rich.live.Live, this fixes it.
def print_md(md_stream):
"Print streamed markdown"
with Live(Spinner("dots", text="Connecting..."), auto_refresh=False) as live:
for part in md_stream: live.update(Markdown(part), refresh=True)Model Setup
System Environment
# aliases = _aliases('bash')
# print(aliases)# print(_sys_info())Tmux
get_pane
def get_pane(
n, pid:NoneType=None
):
Get output from a tmux pane
# p = get_pane(20)
# print(p[:512])get_panes
def get_panes(
n
):
Call self as a function.
# ps = get_panes(20)
# print(ps[:512])co(['tmux', 'display-message', '-p', '#{history-limit}'], text=True).strip()'50000'
tmux_history_lim
def tmux_history_lim(
):
Call self as a function.
tmux_history_lim()50000
get_history
def get_history(
n, pid:str='current'
):
Call self as a function.
Options and ShellSage
get_opts
def get_opts(
opts:VAR_KEYWORD
):
Call self as a function.
opts = get_opts(model=None, log=None, api_base=None, api_key=''); opts{ 'api_base': '',
'api_key': '',
'log': True,
'model': 'claude-opus-4-5-20251101'}Rich’s Live display and input() can’t coexist — Live manages the terminal cursor, and input() needs it too. When Live.stop() is called, it prints its current renderable as static text, which causes duplication since get_res accumulates the full response. The fix: clear Live’s renderable before stopping, flush the accumulated response as static markdown, and reset the buffer. This way streaming resumes fresh after the tool interaction with no overlap.
with_permission
def with_permission(
action_desc
):
Call self as a function.
print(tools[1]('.'))About to View file/directory with the following arguments: {'args': ['.'], 'kwargs': {}}
Directory contents of /home/natedawg/aai-ws/shell_sage/nbs: /home/natedawg/aai-ws/shell_sage/nbs/01_config.ipynb (3.8k) /home/natedawg/aai-ws/shell_sage/nbs/CNAME (0.0k) /home/natedawg/aai-ws/shell_sage/nbs/_quarto.yml (0.3k) /home/natedawg/aai-ws/shell_sage/nbs/nbdev.yml (0.2k) /home/natedawg/aai-ws/shell_sage/nbs/styles.css (0.6k) /home/natedawg/aai-ws/shell_sage/nbs/tmux.conf (1.4k) /home/natedawg/aai-ws/shell_sage/nbs/sidebar.yml (0.1k) /home/natedawg/aai-ws/shell_sage/nbs/index.ipynb (64.4k) /home/natedawg/aai-ws/shell_sage/nbs/00_core.ipynb (49.3k)
get_sage
def get_sage(
model, mode:str='default', search:bool=False, use_safecmd:bool=False
):
Call self as a function.
# m = 'ollama_chat/qwen3:8b'
# ssage = get_sage(m)
# ssage('Howdy!')m = 'claude-sonnet-4-6'
ssage = get_sage(m, search='l')
ssage('Hi, how are ya?', think='l')I’m doing great, thanks for asking! I’m ShellSage, ready to help you navigate the command line. 🐚
Whether you need help with a specific command, want to understand some shell output, or want to level up your terminal skills — just ask!
- id:
chatcmpl-28c116af-9560-419d-a609-5546a31403e1 - model:
claude-sonnet-4-6 - finish_reason:
stop - usage:
Usage(completion_tokens=61, prompt_tokens=3448, total_tokens=3509, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=None, audio_tokens=None, reasoning_tokens=0, rejected_prediction_tokens=None, text_tokens=61, image_tokens=None, video_tokens=None), prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=0, text_tokens=None, image_tokens=None, video_tokens=None, cache_creation_tokens=0, cache_creation_token_details=CacheCreationTokenDetails(ephemeral_5m_input_tokens=0, ephemeral_1h_input_tokens=0)), cache_creation_input_tokens=0, cache_read_input_tokens=0, inference_geo='global', speed=None)
get_res
def get_res(
sage, q, opts
):
Call self as a function.
opts=NS(api_base='', api_key='', think='l')
list(get_res(ssage, 'Use tools to check if we have a .git in the current directory. Respond with yes/no', opts))About to ripgrep a search term with the following arguments: {'argstr': '--files --max-depth 1 --glob .git'}
About to View file/directory with the following arguments: {'path': '.'}
['',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'No',
'No — there is no `.git` in the current directory (`nbs/`), though',
'No — there is no `.git` in the current directory (`nbs/`), though one likely exists in the parent `shell_sage/` directory.',
'No — there is no `.git` in the current directory (`nbs/`), though one likely exists in the parent `shell_sage/` directory.',
'No — there is no `.git` in the current directory (`nbs/`), though one likely exists in the parent `shell_sage/` directory.']
print_md(get_res(ssage, 'Hi!', opts))Hey! 👋 Ready to help with any shell or command-line questions you have. What's up?
print_md(get_res(ssage, 'Please use your view command to see what files are in the current directory. Only respond with a single paragraph', opts))The current directory (/home/natedawg/aai-ws/shell_sage/nbs) contains the following files: two Jupyter notebooks (00_core.ipynb at 50.2k and index.ipynb at 64.4k, and 01_config.ipynb at 3.8k), a CNAME file (empty), configuration files _quarto.yml and nbdev.yml, a sidebar.yml, a styles.css, and a tmux.conf.
print_md(get_res(ssage, 'Please search the web for interesting facts about Linux. Only respond with a single paragraph.', opts));Here are some fascinating facts about Linux! Linux was created by Linus Torvalds, a Finnish computer science student, in 1991 as a hobby project while studying at the University of Helsinki. Linux very nearly wasn't called Linux — Linus wanted to call it "FreaX," but was persuaded otherwise by the owner of the server hosting his early code, who preferred the name "Linux" (a combination of "Linus" and "Unix"). Out of the top 500 fastest supercomputers in the world, Linux or its variants power 485 of those machines. Space programs by NASA, SpaceX, and many other space agencies rely on Linux, and the International Space Station has run Linux since 2013. There's even an Easter egg hidden in the Linux reboot() system call — it has checks to prevent accidental reboots, with one argument that only accepts the value 0xfee1dead. Linus Torvalds also invented Git, the version control software that became one of the biggest tools in the IT world. And perhaps the quirkiest fact: "Linux" is also a genuine washing powder brand in Switzerland, sharing its name with Torvalds' kernel!
Logging
mk_db
def mk_db(
):
Call self as a function.
Log
def Log(
args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):
Initialize self. See help(type(self)) for accurate signature.
# db = mk_db()
# log = db.logs.insert(Log(timestamp=datetime.now().isoformat(), query='Hi, who are you?', model='llama3.2',
# response='I am ShellSage, a command-line teaching assistant!', mode='default'))
# logMain
main
def main(
query:str <The query to send to the LLM>, v:<Print version>='%(prog)s 1.0.8',
pid:str='current', # `current`, `all` or tmux pane_id (e.g. %0) for context
skip_system:bool=False, # Whether to skip system information in the AI's context
history_lines:int=None, # Number of history lines. Defaults to tmux scrollback history length
mode:str='default', # Available ShellSage modes: ['default', 'sassy']
model:str=None, # The LLM model that will be invoked on the LLM provider
search:str=None, # Wheather to allow the LLM to search the internet
api_base:str=None, api_key:str=None,
think:str=None, # Reasoning effort level: 'l', 'm', 'h' (for supported models)
trust:str=None, # Comma-delimited list of tools to always allow (e.g. "view,rg")
code_theme:str=None, # The code theme to use when rendering ShellSage's responses
code_lexer:str=None, # The lexer to use for inline code markdown blocks
raw:bool=False, # Skip markdown rendering and print plain text
):
Call self as a function.
main(['Do you have a `bash` tool?.'], history_lines=0)Yes, I have a bash tool that can run shell commands safely. It supports most standard unix commands, git operations, pipes, and shell operators. However, it has an allow-list of safe commands and blocks output file redirection (>) to prevent accidental overwrites.
r = f'''
Hello, user! Here are some code blocks:
```python
for i in range(10): print(i)
```
```
This doesn't even have a language definition!
```
```bash
ls **/*
```
'''db = mk_db()
db.logs.insert(Log(timestamp=datetime.now().isoformat(), query='', response=r, model='', mode=''))Log(id=1, timestamp='2025-12-03T04:46:49.790878', query='', response="\nHello, user! Here are some code blocks:\n\n```python\nfor i in range(10): print(i)\n```\n\n```\nThis doesn't even have a language definition!\n```\n\n```bash\nls **/*\n```\n", model='', mode='')
extract_cf
def extract_cf(
idx
):
Call self as a function.
extract_cf(0)'for i in range(10): print(i)'
extract
def extract(
idx:int, # Index of code block to extract
copy:bool=False, # Copy to clipboard
do_print:bool=False, # Print (useful for readline custom shortcuts)
):
Extracts the idx’th codefence from the last shell sage response and sends it to tmux by default