agsmith
v0.8.1
Published
Smith CLI — your extensible agent toolkit
Downloads
570
Readme
Agent Smith
Smith CLI — your extensible agent toolkit.
Installation
npm install
npm run build
npm linkRequires Node.js >= 18.
Configuration
All config and credentials live in ~/.agsmith/ (override the directory by setting AGSMITH_CONFIG_DIR).
~/.agsmith/
config.json # Slack + OpenClaw + Outlook settings
credentials/
gmail-client.json # Gmail OAuth client secret
gmail-user-readonly-token.json # Gmail read-only token
gmail-user-modify-token.json # Gmail modify token
outlook-token-cache.json # Outlook (Microsoft Graph) MSAL token cacheCommands
agsmith config
agsmith config show Show current config with secrets masked
agsmith config path Print the path to the config fileagsmith slack
Setup
agsmith slack set api-key <value> Set the Slack bot/user OAuth token (xoxb-... or xoxp-...)
agsmith slack set main-user-id <value> Set the primary Slack user ID (e.g. U0123456789)Commands
agsmith slack channel list List all channels in the workspace
--include-archived Include archived channels
--private-only Show only private channels
--public-only Show only public channels
--wrap Wrap cell content in table output
--truncate Truncate cell content to column widths
agsmith slack channel create <name> Create a channel in the workspace
--private Create as a private channel
agsmith slack send <channel> <text...> Send a message to a channel<channel> accepts a channel name with or without a leading #, or a channel ID. Names are
resolved to an ID via conversations.list before posting, so the bot token needs
channels:read (and groups:read for private channels) in addition to chat:write.
agsmith gmail
Setup
agsmith gmail import-credentials <path> Copy OAuth client secret to ~/.agsmith/credentials/gmail-client.json
agsmith gmail auth Run the OAuth flow (read-only, default)
--readonly Request read-only access (default)
--modify Request modify access (required for label operations)Tokens are saved automatically to ~/.agsmith/credentials/.
Commands
agsmith gmail list List Gmail messages
--filter <filter> Gmail search query (e.g. "is:unread")
--limit <n> Max messages to fetch (default: 20)
--offset <n> Number of messages to skip (default: 0)
--json Output JSON
--wrap Wrap cell content in table output
--truncate Truncate cell content to column widths
agsmith gmail message <message-id> Show a single message by ID
--json Output JSON
--pdf [output-path] Render the message to a PDF file
agsmith gmail labels List all Gmail labels
--json Output JSON
--wrap Wrap cell content in table output
--truncate Truncate cell content to column widths
agsmith gmail update-labels [message-ids...] Add or remove labels on messages
--filter <filter> Gmail search query to select messages
--add <label-id> Label ID to add (repeatable)
--remove <label-id> Label ID to remove (repeatable)
--add-name <label-name> Label name to add (repeatable)
--remove-name <label-name> Label name to remove (repeatable)
--json Output JSON
agsmith gmail attachment list <message-id> List attachments on a message
--json Output JSON
agsmith gmail attachment download <message-id> <attachment-id> Download an attachment
--output-dir <dir> Directory to write the file into (default: .)
--filename <name> Override the output filename
--json Output JSONagsmith outlook
Read-only access to Outlook / Microsoft 365 mail via Microsoft Graph.
Setup
You'll need an Azure Entra app registration (free — see the Microsoft docs for "App registrations"). Register a public client / native app with the redirect URI http://localhost, enable "Allow public client flows", and grant the delegated permissions Mail.Read, User.Read, and offline_access. Then save your client ID:
agsmith outlook set client-id <azure-app-client-id> Required
agsmith outlook set tenant-id <tenant-id> Optional (default: "common"); set to a tenant
GUID if your app is single-tenant
agsmith outlook set token-cache-path <path> Optional; override the MSAL token cache location
agsmith outlook auth Run the OAuth flow in the browserThe MSAL token cache is saved automatically to ~/.agsmith/credentials/outlook-token-cache.json.
Commands
agsmith outlook list List Outlook messages via Microsoft Graph
--filter <odata> Raw OData $filter expression
--search <query> Free-text $search (subject/body/from/etc.)
--from <address> Shortcut for $filter on from/emailAddress/address
--to <address> Shortcut for $filter on toRecipients
--subject <text> Shortcut for $filter contains(subject, ...)
--in <folder> Well-known folder (inbox, sentitems, drafts,
deleteditems, junkemail, archive)
--unread $filter isRead eq false
--starred $filter flag/flagStatus eq 'flagged'
--has-attachment $filter hasAttachments eq true
--before <date> $filter receivedDateTime lt <date> (YYYY-MM-DD)
--after <date> $filter receivedDateTime ge <date> (YYYY-MM-DD)
--on <date> $filter receivedDateTime within a single day
--limit <n> Max messages to fetch (default: 20)
--offset <n> Number of messages to skip (default: 0)
--json Output JSON
--wrap Wrap cell content in table output
--truncate Truncate cell content to column widths
agsmith outlook message <message-id> Show a single message by Graph ID
--json Output JSON
--pdf [output-path] Render the message to a PDF file (inline cid: images
and image attachments are embedded)agsmith openclaw
Setup
agsmith openclaw set config-dir <path> Set the path to the OpenClaw config directory
agsmith openclaw set default-model <model> Set the default model for new agentsCommands
agsmith openclaw agent create <agent-name>Creates an OpenClaw agent, a matching Slack channel, and binds them together. If the channel or agent already exists, the existing resources are reused.
agsmith iterm
Requires macOS with iTerm2 installed.
agsmith iterm list List all iTerm2 windows, tabs, and sessions,
showing each session's agsmith tag
--json Output JSON
agsmith iterm new <session> [cwd] [command] [args...]
Create a new tab, optionally cd and run a command
agsmith iterm read <session> Read the visible screen contents of a named session
agsmith iterm rename <session> <new-name>
Rename a named session
agsmith iterm run <session> <command> [args...]
Run a command in a named session (sends Ctrl+C first)
agsmith iterm send <session> <text...> Type raw text into a session (no Ctrl+C, no auto-Enter)
-e, --enter Press Enter after the text
agsmith iterm send-ctrl <session> <keys...>
Send control codes / named keys, in order
Single letters become Ctrl+<letter> (`c` = Ctrl+C);
named keys: enter, return, esc, tab, space,
backspace, bs, newline
--delay <ms> Delay between keys (default 400)
agsmith iterm split <direction> <target> [session]
Split a named session horizontally or vertically
<direction> is `horizontal` or `vertical`
agsmith iterm tag <target> <tag> Tag a session so it stays addressable after its
title changes
--index Read <target> as a window.tab.session index from
`iterm list` (session part defaults to 1)Sessions created by iterm new and iterm split are tagged with the user.agsmithTerminal
iTerm2 variable, and every <session> lookup above matches that tag before falling back to the
session name — names get rewritten by whatever runs in the tab. See agsmith cc below.
Use iterm tag to adopt a session agsmith did not create, or one whose title has already
drifted. Both of these tag the same session, after which TMS works as the <session>
argument to every command above:
agsmith iterm tag "✳ TMS" TMS # by current title, quoting it exactly
agsmith iterm tag 1.13 TMS --index # by position: window 1, tab 13, session 1Look-up by name only matches untagged sessions, since a tag always wins. To re-tag a session
that already has one, pass its current tag as <target>, or address it by --index.
agsmith cc
Claude Code session helpers.
agsmith cc rc <path> Open a new iTerm2 tab, cd to <path>, and launch
`claude --remote-control "<session>"`
-t, --terminal <name> iTerm2 terminal to reuse (no new tab). If no such
terminal exists, a new tab with this name is created
-s, --session <name> Remote-control session name (default: <dirname>-MMDD)
-p, --prompt <text> Initial prompt to send to Claude on startup
-r, --resume [id] Resume this terminal's Claude conversation; pass a
session id to resume a specific oneWith no flags, a new tab is opened, named after the target directory, and the remote-control
session is <dirname>-MMDD.
-t never restarts a Claude that is already running. What it does depends on the terminal:
| Terminal state | What happens |
|---|---|
| Does not exist | New tab is created, then claude --remote-control is launched in it |
| Exists, running Claude | --prompt is typed into that Claude session; --session is ignored |
| Exists, running Claude, no --prompt | Nothing — reports that Claude is already running |
| Exists, plain shell | cds and launches claude --remote-control in it |
"Running Claude" is detected by looking for a claude process on the session's tty. The iTerm2
session.jobName variable is not usable for this: it reports the deepest child process, so a
Claude session that has spawned an LSP or caffeinate reports that child instead.
Checking on sessions
agsmith cc status [terminal] Report the state of Claude sessions in iTerm2
--all Include terminals not running Claude
--json Output JSON┌──────────────────┬─────────┬────────┬────────┬──────────────┬───────────────┐
│ Terminal │ State │ Remote │ Tagged │ Conversation │ Last activity │
│ copybara-1 │ idle │ yes │ yes │ 8f6674ba │ 1h 50m ago │
│ ✳ TMS │ idle │ no │ no │ - │ 10m ago │
└──────────────────┴─────────┴────────┴────────┴──────────────┴───────────────┘State is working when the status bar offers esc to interrupt, prompt when Claude is
waiting on a menu choice (folder trust, MCP approval, resume size) and will answer nothing
until it is dealt with, and idle otherwise. The status bar is the signal to trust: a
replayed transcript can look mid-task on screen while Claude sits idle.
Unsent composer text is deliberately not reported. Claude renders inline autocomplete suggestions in the composer, and iTerm2 hands over screen contents with all styling stripped, so the dim text of a suggestion is indistinguishable from text you typed. Anything claiming to show unsent input would report Claude's own suggestions as your work.
Remote reports whether remote control is still attached — a session can be running and
healthy with remote control dropped, which is what a disconnected session usually is.
Last activity is the mtime of the conversation transcript.
Claude is launched as env -u CLAUDE_CODE_CHILD_SESSION CLAUDE_CODE_FORCE_SESSION_PERSISTENCE=1
claude .... A terminal that inherits CLAUDE_CODE_CHILD_SESSION starts Claude with its
transcript disabled, which silently breaks --resume and Last activity — both read the
transcript — so the variable is cleared and persistence forced at launch.
Reading a session's conversation id
agsmith cc session-id <terminal> Read the id from Claude's /status panel and record it
--no-record Print it without storing it on the terminalThis is the most reliable source: /status labels the id outright, and unlike the line
Claude prints on exit — which is a session name for a named session, and a name is a picker
search term rather than an id — it is readable while the session is running. It types
/status into the session and dismisses the panel with Esc, so it only works on a live
session. The recorded id is what -r uses afterwards, so capture it before exiting.
Restarting a session
Every session cc rc starts is given its conversation id up front (claude --session-id), and
that id is stored on the terminal as user.agsmithClaudeSession. To restart a terminal whose
Claude has exited, and get its conversation back:
agsmith iterm send-ctrl <terminal> c c --delay 600 # double Ctrl+C exits Claude
agsmith cc rc <path> -t <terminal> -rDo not reach for claude --continue when a directory hosts more than one session: it resumes
the most recently modified conversation for that directory, which is whichever session last
wrote a message — not the one belonging to the terminal you ran it in. -r is unambiguous
because the id is recorded per terminal.
-r also works when the terminal is gone entirely — the usual state of a session that shows
as offline. It creates the tab and resumes in one step, identifying the conversation from the
remote-control name, which is also its custom title:
agsmith cc rc ~/dev/ts-ird -t ts-ird -s ts-ird-0711 -r-r resolves the conversation in this order: an id passed to --resume, the id recorded on
the terminal, the resume line on the terminal's screen, the custom title matching
--session, and finally the sole conversation for the directory. When none of those settle
it, it lists the candidates so one can be passed to --resume <id> rather than guessing.
For a session agsmith did not start there is no recorded id, so -r reads the
Resume this session with: claude --resume <value> line Claude prints on exit. Both exit
paths print it, the id is stable across resumes, and -r records what it finds so later
restarts do not depend on the line still being on screen.
That printed value is not always an id. A session carrying a custom title is echoed by
name — claude --resume "copybara-1" — and a name is a picker search term, not an id:
handing it back to claude --resume opens an interactive list. -r instead maps the name to
an id offline, by reading the {"type":"custom-title"} records in the transcripts under
~/.claude/projects/<cwd with / replaced by ->.
Names are reused, so a name can match several conversations — every restart under the same
title adds one. -r lists the matches, uses the most recently modified, and tells you to pass
--resume <id> to choose another:
Session name "copybara-2" matches 4 conversations; using the most recently modified:
679c2f6b-d5ee-4b01-b8fe-d743f5760ee6 2026-08-10 07:42 <- using this
5c3fa544-260a-496b-9ec1-8e13eb17ae3b 2026-08-04 13:31
...Resuming a long conversation can prompt for confirmation — Claude warns when a session is
large and offers to resume from a summary instead. -r cannot answer that for you, so check
the terminal afterwards with agsmith iterm read <terminal>.
Terminals touched by cc rc, iterm new, and iterm split are tagged with the iTerm2 user
variable user.agsmithTerminal, which -t and all iterm lookups match on first. Session
names are rewritten by whatever runs in the tab — Claude Code retitles them to ✳ Claude
Code and then to a summary of the conversation — so the tag is what makes a terminal reliably
addressable. Untagged sessions, including any created by hand, still match on name.
