@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.
AsyncChat
def AsyncChat(
arg:VAR_POSITIONAL, kw:VAR_KEYWORD
):
Lazy load lisette to make ssage more responsive
Jupyter does work with rich.live.Live, this fixes it.
async def print_md(md_stream):
"Print streamed markdown"
with Live(Spinner("dots", text="Connecting..."), auto_refresh=False) as live:
async 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()'2000'
tmux_history_lim
def tmux_history_lim(
):
Call self as a function.
tmux_history_lim()2000
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': False, 'model': 'claude-sonnet-4-6'}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 /Users/keremturgutlu/aai-ws/shell_sage/nbs: /Users/keremturgutlu/aai-ws/shell_sage/nbs/00_core.ipynb (30.4k) /Users/keremturgutlu/aai-ws/shell_sage/nbs/_quarto.yml (0.3k) /Users/keremturgutlu/aai-ws/shell_sage/nbs/sidebar.yml (0.1k) /Users/keremturgutlu/aai-ws/shell_sage/nbs/styles.css (0.6k) /Users/keremturgutlu/aai-ws/shell_sage/nbs/CNAME (0.0k) /Users/keremturgutlu/aai-ws/shell_sage/nbs/01_config.ipynb (4.1k) /Users/keremturgutlu/aai-ws/shell_sage/nbs/tmux.conf (1.4k) /Users/keremturgutlu/aai-ws/shell_sage/nbs/nbdev.yml (0.2k) /Users/keremturgutlu/aai-ws/shell_sage/nbs/index.ipynb (64.4k)
get_sage
def get_sage(
model, mode:str='default', search:bool=False, use_safecmd:bool=False, backend:str='lisette',
vendor_name:NoneType=None
):
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')
await ssage('Hi, how are ya?', think='l')I’m doing great, thanks for asking! I’m ShellSage, your command-line teaching assistant. Ready to help you with any shell commands, scripting questions, or system administration tasks you have. What can I help you with today?
- id:
chatcmpl-6136c285-faae-4543-8b86-63bd33a98545 - model:
claude-sonnet-4-6 - finish_reason:
stop - usage:
Usage(completion_tokens=54, prompt_tokens=3472, total_tokens=3526, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=None, audio_tokens=None, reasoning_tokens=0, rejected_prediction_tokens=None, text_tokens=54, 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)
fssage = get_sage(m, search='l', backend='fastllm')
await fssage('Hi, how are ya?', think='l')I’m doing great, thanks for asking! Ready to help you navigate the command line. 😄
Whether you need help with a tricky command, want to understand some shell scripting, or are trying to debug system issues — just ask!
- model:
claude-sonnet-4-6 - finish_reason:
stop - usage:
Usage(prompt_tokens=5638, completion_tokens=54, total_tokens=5692, cached_tokens=0, cache_creation_tokens=0, reasoning_tokens=0, raw={'input_tokens': 5638, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'cache_creation': {'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_tokens': 54, 'service_tier': 'standard', 'inference_geo': 'global'})
get_res
def get_res(
sage, q, opts
):
Call self as a function.
opts=NS(api_base='', api_key='', think='l', backend='lisette')
[o async for o in 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 -g ".git" .'}
About to View file/directory with the following arguments: {'path': '.'}
['',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'No',
'No — but the parent directory (`shell_sage`) likely',
'No — but the parent directory (`shell_sage`) likely has one.',
'No — but the parent directory (`shell_sage`) likely has one.',
'No — but the parent directory (`shell_sage`) likely has one.']
opts=NS(api_base='', api_key='', think='l', backend='fastllm')
[o async for o in get_res(fssage, 'Use `rg` tool 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 --hidden -g ".git"'}
['No.']
await print_md(get_res(ssage, 'Hi!', opts))⠋ Connecting...
await print_md(get_res(fssage, 'Hi!', opts))Hey! 👋 What shell command or system admin topic can I help you with today?
await 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))⠋ Connecting...
About to View file/directory with the following arguments: {'path': '.'}
await print_md(get_res(fssage, 'Please use your view command to see what files are in the current directory. Only respond with a single paragraph', opts))The current directory (/Users/keremturgutlu/aai-ws/shell_sage/nbs) contains a mix of Jupyter notebooks (00_core.ipynb, 01_config.ipynb, index.ipynb), configuration files (_quarto.yml, nbdev.yml, sidebar.yml), a styles.css stylesheet, a tmux.conf config, and a CNAME file — looks like an nbdev project with Quarto-based documentation!
await print_md(get_res(fssage, 'Please search the web for interesting facts about Linux. Only respond with a single paragraph.', opts));I wasn't able to retrieve the web search results due to a tool limit. However, here are some well-known interesting facts about Linux from my training knowledge: Linux was created by Linus Torvalds in 1991 as a hobby project while he was a student at the University of Helsinki; it powers the vast majority of the world's servers, supercomputers, and Android devices; the Linux kernel has grown to millions of lines of code contributed by thousands of developers worldwide; and despite being free and open-source, it underpins much of the infrastructure behind major companies like Google, Amazon, and Facebook.
await print_md(get_res(ssage, 'Please search the web for interesting facts about Linux. Only respond with a single paragraph.', opts));⠋ Connecting...
await print_md(get_res(fssage, 'Please search the web for interesting facts about Linux. Only respond with a single paragraph.', opts));*Since its creation in 1991, Linux has gone on to revolutionize the world, empower startups, and birth new industries. *Linux very nearly wasn't called Linux — Linus wanted to call his project "FreaX," but was persuaded otherwise by the owner of the server hosting his early code, who preferred the name "Linux." *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. *Today, over 80% of Linux contributions come from developers paid by big enterprises, with Intel topping the list of contributors for most kernel releases. *And for a truly fun fact — "Linux" is also a genuine washing powder brand in Switzerland, a company named the same as the kernel created by Linus Torvalds!
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
async 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
backend:str=None, # Backend to use. Defaults to shell_sage config.
vendor_name:str=None, # Vendor name for fastllm backend (e.g. 'fireworks_ai')
):
Call self as a function.
await main(['Do you have a `bash` tool?.'], history_lines=0)No, I don't have a bash tool. My available tools are: • view — view file/directory contents • rg — run ripgrep searches • create — create new files • str_replace — edit files by replacing text • insert — insert text at a specific line in a file I can read files and search codebases, but I can't execute arbitrary shell commands. For running commands, you'd do that directly in your terminal.
await main(['Do you have a `bash` tool?.'], history_lines=0, backend='fastllm')No, I don't have a bash (or any shell execution) tool. My available tools are: • view — view file/directory contents • rg — search files using ripgrep • create — create new files • str_replace — edit files by replacing text • insert — insert text at a line number I can read and search your codebase and make file edits, but I cannot execute shell commands directly. You run commands on your end, and I can help you understand output or craft the right commands to use.
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=4, timestamp='2026-05-06T16:24:11.912059', 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