A terminal IPython extension that adds Claude and Codex powered prompting
Editable install:
pip install -e ipyai
Run tests:
cd ipyai
./tools/test.sh
The test harness keeps setup small: tools/test.sh redirects XDG_CONFIG_HOME to a repo-local temp dir so ipyai config writes stay out of a normal user config tree, then runs pytest. It does not redirect CLAUDE_CONFIG_DIR, because on macOS claude -p OAuth reads credentials from the login keychain keyed by the userID in ~/.claude.json; redirecting the config dir breaks that lookup. Instead, ClaudeBackend sweeps any session jsonls it can identify as its own (or as known claude side-effect stubs) after each turn. The live Codex app-server test archives the threads it creates after the test so they do not show up in codex resume.
IPyAIController, prompt transforms, SQLite bookkeeping, notebook save/load, prompt mode, keybindings, Rich streaming display, backend selectionclaude -p per turn, writes a synthetic session JSONL for context seeding, bridges custom tools through a unix socket + stdio MCP sidecar, and translates stream-json events into canonical backend eventsToolRegistry (list_tools, call_tool) to the MCP bridge subprocessipyai-mcp-bridge) that claude -p spawns; forwards MCP tool calls over the unix socket_LisetteBackend plus two backends on top of it — ClaudeAPIBackend (Anthropic via lisette) and CodexAPIBackend (Codex responses endpoint via lisette.CodexChat); this is the explicit exception to the common canonical-event formatter path and still uses lisette’s native formatterToolRegistry, schema generation, and local tool calling helpersipyai console entry pointipyai-mcp-bridge and exercises it over real MCP stdioclaude -p --output-format=stream-json)ipyai is a ZMQTerminalIPythonApp subclass. IPyAIApp defines the ipyai-specific flags directly with traitlets (-b, -r, -l, -p, --resume-pick, --keep-alive) while still inheriting standard jupyter_console flags. Bare -r is preprocessed to --resume-pick; -r N resumes the concrete ipyai session id.
. is rewritten into %ipyai.IPyAIController.run_prompt() reconstructs recent code/output/note context from IPython history.$nameand shell refs like `!`cmd are injected above the prompt.core.py first builds a typed ConversationSeedprepare_turn(...)s using that seedclaude -p --resume, and starts a unix-socket MCP bridge for custom tools_LisetteBackendastream_to_stdout() renders the response through Rich in TTY mode and stores the final transcript text locally.Completion policy is shared in BaseBackend.complete():
ConversationSeedprovider_session_id=Nonetool_mode="off"ephemeral=Truethink=COMPLETION_THINK (fixed low effort for inline completions, independent of DEFAULT_THINK)Backends can still override complete() if a provider genuinely requires it, but the default path is now the contract.
There are two layers of state:
ipyai uses:
claude_prompts for AI prompt historysessions.remark JSON for cwd, backend, and provider_session_idIf prompt history exists locally but provider_session_id is missing, provider bootstrap is backend-specific:
--no-session-persistence keeps claude from writing anything further)Notebook save/load is explicit only:
%ipyai save <filename>%ipyai load <filename>ipyai -l <filename>There is no implicit startup.ipynb behavior.
The custom tool story is intentionally small:
pyrun, bash, start_bgterm, write_stdin, close_bgterm, lnhashview_file, exhash_fileBash, Edit, Read, Skill, WebFetch, WebSearch, Writepyrun does not call back into InteractiveShell.run_cell*. It delegates to safepyrun, looked up in shell.user_ns, matching the old ipycodex direct-call boundary and avoiding nested IPython cell execution.
Provider-specific tool exposure now fans out from the shared ToolRegistry:
ipyai-mcp-bridge) exposes the registry to claude -p via --mcp-config; allowed tool names use the mcp__ipy__... prefixlisettedynamicToolsThe ipyai CLI loads safepyrun before ipyai, so normal terminal sessions get pyrun automatically. ipyai seeds the other custom tools into shell.user_ns directly.
Skills are Claude-native:
Skill tool is enabled--setting-sources user,project is passed to claude -p.claude/plugins up the cwd parent chain and passed as repeated --plugin-dir flagsThe samples/ directory holds committed stream-shape artifacts so event-wiring spelunking does not need to be repeated. The capture scripts there still import claude_agent_sdk and are kept only as historical reference; the live Claude backend no longer uses the SDK. To re-capture against claude -p, run it directly with --output-format=stream-json --include-partial-messages --verbose and save the output.
The test suite is intentionally small and integration-heavy.
Current coverage focuses on:
ipyai resolves config paths via XDG.XDG_CONFIG_HOME so config writes stay out of a normal user config tree. CLAUDE_CONFIG_DIR is intentionally not redirected (keychain-based OAuth depends on ~/.claude.json).