A community OpenCode plugin that adds robust support for Jupyter Notebook (.ipynb) files: inspect, read, edit, run, outputs, clean, export, reproducibility reports, and warm Python kernels.
The plugin is opt-in, distributed via npm, and does not modify the OpenCode core. It exposes thirteen agent tools that operate on .ipynb files with explicit permissions, granular context control, and a strongly-typed Effect runtime internally.
Repository: https://github.com/Restodecoca/opencode-ipynb Issues: https://github.com/Restodecoca/opencode-ipynb/issues
- Notebooks are large, binary-like artifacts. Dumping them into the agent context burns tokens.
- Reading outputs and editing cells are high-trust actions that must ask permission every time.
- The execution flow needs a Python runtime, which is not something every OpenCode user wants installed.
- A plugin lets the community iterate on these workflows without forcing changes on the OpenCode core.
After the package is published on npm, add it to your opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-ipynb"]
}For local development, point OpenCode at a checkout or built tarball:
{
"plugin": ["file:./opencode-ipynb"]
}Or run directly from this repo:
git clone https://github.com/Restodecoca/opencode-ipynb
cd opencode-ipynb
bun install
bun run buildThen add "opencode-ipynb" (or the local path) to your opencode.json.
| Tool | Purpose |
|---|---|
ipynb_inspect |
Compact summary: kernel/language metadata, cell table, output stats, execution-order warnings, optional missing-package checks. |
ipynb_read |
Granular read of one cell or a range, with optional outputs/errors/metadata/images and truncation limits. |
ipynb_edit |
Safe per-cell source edit with textual diff, output clearing policy, permission gate, and per-file lock. |
ipynb_cell_insert |
Insert a code/markdown/raw cell before or after a target index. |
ipynb_cell_delete |
Delete one cell by index and return a preview of the deleted source. |
ipynb_cell_move |
Move one cell to a new index. |
ipynb_run |
Execute one cell, a range, from a cell, or a full notebook via the Python helper. Supports save-to-notebook and optional warm kernels. |
ipynb_outputs |
List / read / read_error / clear_cell / clear_all outputs with pagination and permission gates for writes. |
ipynb_clean |
Strip outputs, execution counts, widget state, large images, and normalize source arrays for Git-friendly diffs. |
ipynb_export |
Export to Markdown, Python, or summary; optionally write to disk after permission. |
ipynb_doctor |
Diagnose Python, helper discovery, and required Jupyter dependencies. |
ipynb_repro |
Reproducibility report: kernel/env info, pip freeze, filesystem reads, random seeds, non-determinism, missing packages. |
ipynb_kernel |
Inspect/restart/shutdown warm kernels when ipynb.warmKernel: true is enabled. |
Inspect a notebook before doing anything else:
ipynb_inspect({ filePath: "notebooks/eda.ipynb" })
Read a single cell:
ipynb_read({ filePath: "notebooks/eda.ipynb", cellIndex: 7 })
Read a range including outputs:
ipynb_read({
filePath: "notebooks/eda.ipynb",
start: 0,
end: 5,
includeOutputs: true,
maxOutputChars: 4000
})
Edit one cell, clear outputs automatically:
ipynb_edit({
filePath: "notebooks/eda.ipynb",
cellIndex: 7,
source: "import pandas as pd\ndf = pd.read_csv('data.csv')\ndf.head()"
})
Clean for a clean diff before commit:
ipynb_clean({ filePath: "notebooks/eda.ipynb" })
Export to a Python script next to the notebook:
ipynb_export({
filePath: "notebooks/eda.ipynb",
format: "python",
outputPath: "notebooks/eda.py"
})
- Every write (
ipynb_edit,ipynb_clean,ipynb_outputs clear_*,ipynb_export --outputPath) callscontext.askbefore touching the disk. - Every execution (
ipynb_run) callscontext.askwith rich metadata: file path, mode (cell/range/all/from), cell range, save flag, working directory, timeout. PathServicerejects paths that resolve outside of the OpenCode worktree. The lock manager serializes per-file writes to prevent accidental corruption.- The Python execution helper is an opt-in, isolated subprocess; it does not auto-install Jupyter. The user installs
python/requirements.txtonly when they want real execution.
Notebooks can be enormous. By default the plugin never dumps a full notebook into context:
ipynb_inspectreturns a table with the first line of every cell, plus aggregate counts. It never includes the full source.ipynb_readdefaults to not including outputs. SetincludeOutputs: trueto opt in. Images / base64 are always omitted (you only get a size notice).maxSourceChars(default 12 000) andmaxOutputChars(default 6 000) cap the size of any single response.maxTracebackChars(default 8 000) caps error tracebacks.- Every truncation appends
... (truncated, use maxXxx to increase).
The relevant constants live in src/utils/limits.ts.
The plugin never installs Python dependencies automatically. Running ipynb_run is opt-in and requires a Python environment with nbformat, nbclient, jupyter_client, and ipykernel. The plugin only detects what is available and tells you how to fix what is missing.
uv pip install nbformat nbclient jupyter_client ipykernel
python -m ipykernel install --userpython -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install nbformat nbclient jupyter_client ipykernel
python -m ipykernel install --userconda install -c conda-forge nbformat nbclient jupyter_client ipykernelOpenCode may use a different Python interpreter than the one in your terminal. Three ways to tell the plugin which one to use, in priority order:
-
Plugin options (in
opencode.json):{ "plugin": [ [ "opencode-ipynb", { "pythonPath": "C:/Users/Gabriel/miniconda3/envs/data/python.exe", "preferUv": true, "defaultTimeoutMs": 120000, "defaultMaxOutputChars": 6000, "warmKernel": false, "allowOutsideWorktree": false } ] ] }pythonPath(string): absolute path to a Python interpreter.preferUv(boolean, defaulttrue): the doctor and the Python service preferuv pip installoverpip installin their suggestions.defaultTimeoutMs(number, default120000): timeout in milliseconds foripynb_run.defaultMaxOutputChars(number, default6000): fallback foripynb_readandipynb_outputs readwhen the user does not passmaxOutputChars.warmKernel(boolean, defaultfalse): keep a long-lived Python subprocess per notebook path for faster repeatedipynb_run mode=all/mode=cellcalls. Manage it withipynb_kernel.allowOutsideWorktree(boolean, defaultfalse): power-user escape hatch — whentrue, the plugin will read and write.ipynbfiles outside the OpenCode worktree. Leave atfalseunless you know why you need it.
-
Environment variables (the shell wins over plugin options):
OPENCODE_IPYNB_PYTHON=C:/Users/Gabriel/miniconda3/envs/data/python.exe opencode OPENCODE_IPYNB_OPTIONS='{"pythonPath":"/usr/bin/python3","defaultMaxOutputChars":4000}' opencodeOPENCODE_IPYNB_PYTHONoverridesipynb.pythonPathonly.OPENCODE_IPYNB_OPTIONSoverrides the wholeipynb.*block (a JSON object validated by the same schema asopencode.json). These are both set by the plugin on first load if they are not already set in the environment, so an explicit shell value always wins. -
PATH fallback: the plugin tries
python, thenpython3.
If anything goes wrong, run:
ipynb_doctor
It reports the selected interpreter, every Jupyter dependency ([ok] / [missing]), where the helper script was found, and a copy-pasteable install command. It never installs anything.
- Python execution depends on the user's local Python/Jupyter environment. The plugin diagnoses missing packages but never installs them.
- Warm kernels are intentionally limited to
ipynb_run mode=allandmode=cell;range,from, andenvuse one-shot helper processes. - Large notebooks and rich outputs are truncated in tool responses to protect context. When
ipynb_run save=true, full nbformat outputs are preserved on disk. allowOutsideWorktreeis a power-user escape hatch. Keep it disabled unless you explicitly need cross-worktree notebook access.
Three small, runnable notebooks live under test/integration/ and double
as both documentation and a smoke test for the plugin's read / run /
export loop:
test/integration/classification/—LogisticRegressiononsklearn.datasets.load_iris(7 cells, no CSV needed).test/integration/timeseries/—numpymoving average over a noisy sine wave (5 cells, stdlib-friendly).test/integration/scraping/—urllib.request+html.parserfetch ofhttps://example.com/(4 cells, stdlib only, gracefultry/excepton network failure).
Each one ships with outputs: [] and a matching README.md that lists
the exact ipynb_inspect / ipynb_read / ipynb_run calls plus the
expected output.
The published npm tarball contains only the runtime: dist/, python/ipynb_runner.py, python/requirements.txt, README.md, and LICENSE. Internal developer docs, helper dev scripts, and .github/ workflows are excluded from the npm package via .npmignore and are also gitignored so they are not versioned in this repository. Contributors track their work through GitHub Issues and Discussions.
Cut a release by tagging the commit and pushing the tag, then publish it on GitHub — the workflow runs only on the published release event:
git tag v0.1.0 && git push --tagsThen create a GitHub release at https://github.com/Restodecoca/opencode-ipynb/releases/new pointing at v0.1.0. The CI runs bun run typecheck, bun test, and bun run build before npm publish --provenance --access public. The repository must have a single secret configured: NPM_TOKEN (an npm automation token with publish rights for the package name in package.json).
Before publishing, run a local package check:
bun run typecheck
bun test
bun run build
npm pack --dry-runThe package name opencode-ipynb is currently available on npm. If you publish under a scope instead, update package.json, the install snippet, and this release tag convention together.
This project uses the Effect Language Service to surface Effect-specific diagnostics at build time and in the editor.
The tsconfig.json already includes the plugin:
{
"compilerOptions": {
"plugins": [{ "name": "@effect/language-service" }]
}
}effect-language-service patch modifies the local node_modules/typescript so that tsc --noEmit reports Effect-specific diagnostics. This is opt-in because it mutates your local install.
bun run prepare:effect-lsp # alias for: effect-language-service patch
bun run typecheck # now also catches "Effect must be yielded" and similarThe project does NOT add this as a prepare script. Patch only when you want it.
For VS Code, Cursor, Zed or NVim, make sure the editor uses the workspace TypeScript (not the bundled one). In VS Code / Cursor, open a .ts file, click the TypeScript version in the status bar, and select Use Workspace Version.
The OpenCode TypeScript LSP (built-in) also picks up the workspace TypeScript and therefore respects the @effect/language-service plugin. A minimal opencode.json in the project root enables it:
{
"$schema": "https://opencode.ai/config.json",
"lsp": {
"typescript": {
"extensions": [".ts", ".tsx", ".mts", ".cts"]
}
}
}bun install
bun run typecheck
bun test
bun run buildLayout:
src/
index.ts # re-exports the plugin
plugin.ts # Plugin object, registers all 13 tools
domain/ # zod schemas + types for cells, outputs, execution
services/ # Effect-style services (Path, Permission, File, ...)
format/ # markdown / output / diagnostic / export formatters
tools/ # one file per tool, thin wrapper over services
utils/ # limits, paths, mime, truncate, json helpers
python/
ipynb_runner.py # JSON-stdin/stdout runner using nbformat + nbclient
requirements.txt
test/
fixtures/ # simple, outputs, error, images
unit/ # bun:test suites
MIT. See LICENSE.