grokbook 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- grokbook-0.1.0/LICENSE +21 -0
- grokbook-0.1.0/PKG-INFO +191 -0
- grokbook-0.1.0/README.md +156 -0
- grokbook-0.1.0/grokbook/__init__.py +0 -0
- grokbook-0.1.0/grokbook/__main__.py +5 -0
- grokbook-0.1.0/grokbook/_server.py +109 -0
- grokbook-0.1.0/grokbook/ansi.py +80 -0
- grokbook-0.1.0/grokbook/api.py +494 -0
- grokbook-0.1.0/grokbook/cli.py +149 -0
- grokbook-0.1.0/grokbook/db.py +594 -0
- grokbook-0.1.0/grokbook/envs.py +525 -0
- grokbook-0.1.0/grokbook/handlers.py +750 -0
- grokbook-0.1.0/grokbook/ipynb.py +210 -0
- grokbook-0.1.0/grokbook/kernel.py +350 -0
- grokbook-0.1.0/grokbook/mcp_server.py +528 -0
- grokbook-0.1.0/grokbook/state.py +30 -0
- grokbook-0.1.0/grokbook/static/js/app.js +668 -0
- grokbook-0.1.0/grokbook/static/js/codemirror.js +51 -0
- grokbook-0.1.0/grokbook/views.py +1239 -0
- grokbook-0.1.0/grokbook/welcome.py +141 -0
- grokbook-0.1.0/grokbook.egg-info/PKG-INFO +191 -0
- grokbook-0.1.0/grokbook.egg-info/SOURCES.txt +26 -0
- grokbook-0.1.0/grokbook.egg-info/dependency_links.txt +1 -0
- grokbook-0.1.0/grokbook.egg-info/entry_points.txt +2 -0
- grokbook-0.1.0/grokbook.egg-info/requires.txt +10 -0
- grokbook-0.1.0/grokbook.egg-info/top_level.txt +1 -0
- grokbook-0.1.0/pyproject.toml +51 -0
- grokbook-0.1.0/setup.cfg +4 -0
grokbook-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marco Jeffrey Pansa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
grokbook-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: grokbook
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Interactive notebook server for learning computer science
|
|
5
|
+
Author-email: Marco Jeffrey Pansa <marco-jeffrey@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/marco-jeffrey/grokbook
|
|
8
|
+
Project-URL: Repository, https://github.com/marco-jeffrey/grokbook
|
|
9
|
+
Project-URL: Issues, https://github.com/marco-jeffrey/grokbook/issues
|
|
10
|
+
Keywords: notebook,jupyter,ipython,datastar,sse,reactive,education
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Education
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Classifier: Intended Audience :: Education
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Framework :: Jupyter
|
|
19
|
+
Classifier: Environment :: Web Environment
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Requires-Python: >=3.14
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: stario==2.3.0
|
|
25
|
+
Requires-Dist: jupyter-client>=8.0.0
|
|
26
|
+
Requires-Dist: ipykernel>=6.25.0
|
|
27
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
28
|
+
Requires-Dist: watchfiles>=1.1.1
|
|
29
|
+
Requires-Dist: markdown-it-py[linkify,plugins]>=4.0.0
|
|
30
|
+
Requires-Dist: fastmcp>=3.1.1
|
|
31
|
+
Requires-Dist: httpx>=0.28.1
|
|
32
|
+
Requires-Dist: tqdm>=4.67.3
|
|
33
|
+
Requires-Dist: typer>=0.15.0
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# grokbook
|
|
37
|
+
|
|
38
|
+
Interactive notebook server for learning computer science. Works like Jupyter — code cells, markdown, persistent IPython kernels — with a built-in MCP server so AI tutors can create and manage notebooks for you.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
Requires **Python 3.14+** and [uv](https://docs.astral.sh/uv/).
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/marco-jeffrey/grokbook.git
|
|
46
|
+
cd grokbook
|
|
47
|
+
uv sync
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
grokbook
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
That's it. Opens the notebook UI on [localhost:8080](http://localhost:8080) and the MCP server on port 8081. A welcome notebook is created on first run.
|
|
57
|
+
|
|
58
|
+
### Custom kernel environment
|
|
59
|
+
|
|
60
|
+
By default, grokbook uses its own Python for the kernel. To use a separate environment with your libraries:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Create an env with your packages
|
|
64
|
+
mkdir /tmp/my-env && cd /tmp/my-env
|
|
65
|
+
uv init && uv add pandas numpy matplotlib ipykernel
|
|
66
|
+
|
|
67
|
+
# Start grokbook with that env
|
|
68
|
+
grokbook serve --python /tmp/my-env/.venv/bin/python
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Remote access (Tailscale / LAN)
|
|
72
|
+
|
|
73
|
+
By default, grokbook binds to `127.0.0.1` (localhost only). To access from other machines:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
grokbook serve --host 0.0.0.0
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Both the notebook server and MCP server bind to all interfaces. Access from another machine at `http://<ip>:8080`.
|
|
80
|
+
|
|
81
|
+
> **Warning**: Grokbook executes arbitrary Python code. Do not expose it to untrusted networks.
|
|
82
|
+
|
|
83
|
+
### CLI reference
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
grokbook # Start everything (default)
|
|
87
|
+
grokbook serve [OPTIONS] # Start notebook + MCP servers
|
|
88
|
+
--host TEXT # Bind address (default: 127.0.0.1)
|
|
89
|
+
--port, -p INT # Notebook server port (default: 8080)
|
|
90
|
+
--mcp-port INT # MCP server port (default: 8081)
|
|
91
|
+
--python PATH # Python interpreter for kernels
|
|
92
|
+
--db PATH # Database file (default: ~/.grokbook/grokbook.db)
|
|
93
|
+
--allow-code-execution # Enable execute/kernel tools in MCP
|
|
94
|
+
|
|
95
|
+
grokbook mcp [OPTIONS] # MCP server standalone (stdio, for Claude Desktop)
|
|
96
|
+
--allow-code-execution # Enable execute/kernel tools
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## MCP Integration
|
|
100
|
+
|
|
101
|
+
On startup, grokbook prints an MCP config block you can paste directly into Claude Desktop or LM Studio:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"mcpServers": {
|
|
106
|
+
"grokbook": {
|
|
107
|
+
"command": "grokbook",
|
|
108
|
+
"args": ["mcp", "--allow-code-execution"],
|
|
109
|
+
"env": {
|
|
110
|
+
"GROKBOOK_API_URL": "http://localhost:8080/api"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Omit `--allow-code-execution` to restrict the MCP server to read/write operations only (no code execution).
|
|
118
|
+
|
|
119
|
+
The `grokbook mcp` command runs in stdio mode for Claude Desktop. For HTTP-based MCP clients (LM Studio, remote agents), the built-in MCP server on port 8081 is already running when you start `grokbook serve`.
|
|
120
|
+
|
|
121
|
+
**Always available**: `list_notebooks`, `get_notebook`, `create_notebook`, `rename_notebook`, `duplicate_notebook`, `list_projects`, `create_project`, `rename_project`, `move_notebook`, `create_cell`, `insert_cell`, `read_cell`, `write_cell`, `delete_cell`, `move_cell`, `duplicate_cell`, `change_cell_type`, `clear_output`, `clear_all_outputs`
|
|
122
|
+
|
|
123
|
+
**With `--allow-code-execution`**: `execute_cell`, `run_all_cells`, `kernel_status`, `restart_kernel`, `get_variables`, `interrupt_kernel`
|
|
124
|
+
|
|
125
|
+
To enable code execution via MCP:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
grokbook serve --allow-code-execution
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Features
|
|
132
|
+
|
|
133
|
+
- **Code cells** with streaming execution, rich output (images, HTML, SVG, pandas tables)
|
|
134
|
+
- **Markdown cells** with GitHub-flavored rendering
|
|
135
|
+
- **Persistent IPython kernels** — one per notebook, variables carry over between cells
|
|
136
|
+
- **Keyboard-driven** — Vim-like command/edit modes (j/k, a/b, dd, Shift+Enter)
|
|
137
|
+
- **Import/export** Jupyter `.ipynb` files
|
|
138
|
+
- **Variables inspector** panel
|
|
139
|
+
- **Dark/light theme**, wide mode, autocomplete, signature tooltips
|
|
140
|
+
- **Live sync** across browser tabs via SSE
|
|
141
|
+
|
|
142
|
+
## Keyboard Shortcuts
|
|
143
|
+
|
|
144
|
+
Grokbook uses two modes, inspired by Vim:
|
|
145
|
+
|
|
146
|
+
**Command mode** (press `Escape` to enter):
|
|
147
|
+
|
|
148
|
+
| Key | Action |
|
|
149
|
+
|-----|--------|
|
|
150
|
+
| `j` / `k` | Navigate between cells |
|
|
151
|
+
| `Enter` | Edit selected cell |
|
|
152
|
+
| `a` / `b` | Insert cell above / below |
|
|
153
|
+
| `m` | Convert to markdown |
|
|
154
|
+
| `y` | Convert to code |
|
|
155
|
+
| `dd` | Delete cell |
|
|
156
|
+
| `Cmd+Shift+Up/Down` | Move cell up / down |
|
|
157
|
+
|
|
158
|
+
**Edit mode** (press `Enter` or click a cell):
|
|
159
|
+
|
|
160
|
+
| Key | Action |
|
|
161
|
+
|-----|--------|
|
|
162
|
+
| `Shift+Enter` | Execute cell, move to next |
|
|
163
|
+
| `Cmd+Enter` / `Ctrl+Enter` | Execute cell, stay in place |
|
|
164
|
+
| `Escape` | Back to command mode |
|
|
165
|
+
| `Tab` / `Shift+Tab` | Indent / dedent |
|
|
166
|
+
|
|
167
|
+
### Vim mode
|
|
168
|
+
|
|
169
|
+
Enable Vim keybindings from the editor settings panel (gear icon). When active:
|
|
170
|
+
|
|
171
|
+
- Full Vim motions in code cells (normal, insert, visual modes)
|
|
172
|
+
- `jk` is mapped to `Escape` in insert mode for quick mode switching
|
|
173
|
+
- Block cursor in normal mode, line cursor in insert mode
|
|
174
|
+
|
|
175
|
+
## Architecture
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
Browser ──SSE──▶ Stario server (:8080) ──ZMQ──▶ IPython kernel
|
|
179
|
+
│ │
|
|
180
|
+
│ Datastar │ SQLite (~/.grokbook/grokbook.db)
|
|
181
|
+
│ (reactive │
|
|
182
|
+
│ signals) ├── REST API (/api)
|
|
183
|
+
│ │
|
|
184
|
+
▼ ▼
|
|
185
|
+
DOM patches MCP server (:8081)
|
|
186
|
+
via SSE (FastMCP, for LLM agents)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT
|
grokbook-0.1.0/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# grokbook
|
|
2
|
+
|
|
3
|
+
Interactive notebook server for learning computer science. Works like Jupyter — code cells, markdown, persistent IPython kernels — with a built-in MCP server so AI tutors can create and manage notebooks for you.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Requires **Python 3.14+** and [uv](https://docs.astral.sh/uv/).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/marco-jeffrey/grokbook.git
|
|
11
|
+
cd grokbook
|
|
12
|
+
uv sync
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
grokbook
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That's it. Opens the notebook UI on [localhost:8080](http://localhost:8080) and the MCP server on port 8081. A welcome notebook is created on first run.
|
|
22
|
+
|
|
23
|
+
### Custom kernel environment
|
|
24
|
+
|
|
25
|
+
By default, grokbook uses its own Python for the kernel. To use a separate environment with your libraries:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Create an env with your packages
|
|
29
|
+
mkdir /tmp/my-env && cd /tmp/my-env
|
|
30
|
+
uv init && uv add pandas numpy matplotlib ipykernel
|
|
31
|
+
|
|
32
|
+
# Start grokbook with that env
|
|
33
|
+
grokbook serve --python /tmp/my-env/.venv/bin/python
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Remote access (Tailscale / LAN)
|
|
37
|
+
|
|
38
|
+
By default, grokbook binds to `127.0.0.1` (localhost only). To access from other machines:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
grokbook serve --host 0.0.0.0
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Both the notebook server and MCP server bind to all interfaces. Access from another machine at `http://<ip>:8080`.
|
|
45
|
+
|
|
46
|
+
> **Warning**: Grokbook executes arbitrary Python code. Do not expose it to untrusted networks.
|
|
47
|
+
|
|
48
|
+
### CLI reference
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
grokbook # Start everything (default)
|
|
52
|
+
grokbook serve [OPTIONS] # Start notebook + MCP servers
|
|
53
|
+
--host TEXT # Bind address (default: 127.0.0.1)
|
|
54
|
+
--port, -p INT # Notebook server port (default: 8080)
|
|
55
|
+
--mcp-port INT # MCP server port (default: 8081)
|
|
56
|
+
--python PATH # Python interpreter for kernels
|
|
57
|
+
--db PATH # Database file (default: ~/.grokbook/grokbook.db)
|
|
58
|
+
--allow-code-execution # Enable execute/kernel tools in MCP
|
|
59
|
+
|
|
60
|
+
grokbook mcp [OPTIONS] # MCP server standalone (stdio, for Claude Desktop)
|
|
61
|
+
--allow-code-execution # Enable execute/kernel tools
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## MCP Integration
|
|
65
|
+
|
|
66
|
+
On startup, grokbook prints an MCP config block you can paste directly into Claude Desktop or LM Studio:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"grokbook": {
|
|
72
|
+
"command": "grokbook",
|
|
73
|
+
"args": ["mcp", "--allow-code-execution"],
|
|
74
|
+
"env": {
|
|
75
|
+
"GROKBOOK_API_URL": "http://localhost:8080/api"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Omit `--allow-code-execution` to restrict the MCP server to read/write operations only (no code execution).
|
|
83
|
+
|
|
84
|
+
The `grokbook mcp` command runs in stdio mode for Claude Desktop. For HTTP-based MCP clients (LM Studio, remote agents), the built-in MCP server on port 8081 is already running when you start `grokbook serve`.
|
|
85
|
+
|
|
86
|
+
**Always available**: `list_notebooks`, `get_notebook`, `create_notebook`, `rename_notebook`, `duplicate_notebook`, `list_projects`, `create_project`, `rename_project`, `move_notebook`, `create_cell`, `insert_cell`, `read_cell`, `write_cell`, `delete_cell`, `move_cell`, `duplicate_cell`, `change_cell_type`, `clear_output`, `clear_all_outputs`
|
|
87
|
+
|
|
88
|
+
**With `--allow-code-execution`**: `execute_cell`, `run_all_cells`, `kernel_status`, `restart_kernel`, `get_variables`, `interrupt_kernel`
|
|
89
|
+
|
|
90
|
+
To enable code execution via MCP:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
grokbook serve --allow-code-execution
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Features
|
|
97
|
+
|
|
98
|
+
- **Code cells** with streaming execution, rich output (images, HTML, SVG, pandas tables)
|
|
99
|
+
- **Markdown cells** with GitHub-flavored rendering
|
|
100
|
+
- **Persistent IPython kernels** — one per notebook, variables carry over between cells
|
|
101
|
+
- **Keyboard-driven** — Vim-like command/edit modes (j/k, a/b, dd, Shift+Enter)
|
|
102
|
+
- **Import/export** Jupyter `.ipynb` files
|
|
103
|
+
- **Variables inspector** panel
|
|
104
|
+
- **Dark/light theme**, wide mode, autocomplete, signature tooltips
|
|
105
|
+
- **Live sync** across browser tabs via SSE
|
|
106
|
+
|
|
107
|
+
## Keyboard Shortcuts
|
|
108
|
+
|
|
109
|
+
Grokbook uses two modes, inspired by Vim:
|
|
110
|
+
|
|
111
|
+
**Command mode** (press `Escape` to enter):
|
|
112
|
+
|
|
113
|
+
| Key | Action |
|
|
114
|
+
|-----|--------|
|
|
115
|
+
| `j` / `k` | Navigate between cells |
|
|
116
|
+
| `Enter` | Edit selected cell |
|
|
117
|
+
| `a` / `b` | Insert cell above / below |
|
|
118
|
+
| `m` | Convert to markdown |
|
|
119
|
+
| `y` | Convert to code |
|
|
120
|
+
| `dd` | Delete cell |
|
|
121
|
+
| `Cmd+Shift+Up/Down` | Move cell up / down |
|
|
122
|
+
|
|
123
|
+
**Edit mode** (press `Enter` or click a cell):
|
|
124
|
+
|
|
125
|
+
| Key | Action |
|
|
126
|
+
|-----|--------|
|
|
127
|
+
| `Shift+Enter` | Execute cell, move to next |
|
|
128
|
+
| `Cmd+Enter` / `Ctrl+Enter` | Execute cell, stay in place |
|
|
129
|
+
| `Escape` | Back to command mode |
|
|
130
|
+
| `Tab` / `Shift+Tab` | Indent / dedent |
|
|
131
|
+
|
|
132
|
+
### Vim mode
|
|
133
|
+
|
|
134
|
+
Enable Vim keybindings from the editor settings panel (gear icon). When active:
|
|
135
|
+
|
|
136
|
+
- Full Vim motions in code cells (normal, insert, visual modes)
|
|
137
|
+
- `jk` is mapped to `Escape` in insert mode for quick mode switching
|
|
138
|
+
- Block cursor in normal mode, line cursor in insert mode
|
|
139
|
+
|
|
140
|
+
## Architecture
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
Browser ──SSE──▶ Stario server (:8080) ──ZMQ──▶ IPython kernel
|
|
144
|
+
│ │
|
|
145
|
+
│ Datastar │ SQLite (~/.grokbook/grokbook.db)
|
|
146
|
+
│ (reactive │
|
|
147
|
+
│ signals) ├── REST API (/api)
|
|
148
|
+
│ │
|
|
149
|
+
▼ ▼
|
|
150
|
+
DOM patches MCP server (:8081)
|
|
151
|
+
via SSE (FastMCP, for LLM agents)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Server bootstrap and runner — shared by main.py (dev) and cli.py (production)."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from stario import Relay, RichTracer, Stario
|
|
9
|
+
from stario.http.server import Server
|
|
10
|
+
from stario.http.writer import CompressionConfig
|
|
11
|
+
from stario.telemetry.core import Span
|
|
12
|
+
|
|
13
|
+
from grokbook.api import api_router
|
|
14
|
+
from grokbook.db import Database
|
|
15
|
+
from grokbook import envs
|
|
16
|
+
from grokbook.handlers import app_router
|
|
17
|
+
from grokbook.kernel import KernelPool
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def make_bootstrap(db_path: Path, python_path: str | None = None):
|
|
21
|
+
@asynccontextmanager
|
|
22
|
+
async def bootstrap(app: Stario, span: Span):
|
|
23
|
+
nonlocal python_path
|
|
24
|
+
db = await Database.connect(str(db_path))
|
|
25
|
+
|
|
26
|
+
# Create welcome notebook on first run
|
|
27
|
+
from grokbook.welcome import ensure_welcome_notebook
|
|
28
|
+
|
|
29
|
+
await ensure_welcome_notebook(db)
|
|
30
|
+
|
|
31
|
+
# Discover available Python environments (uv + kernelspecs + cwd .venv)
|
|
32
|
+
await envs.refresh()
|
|
33
|
+
|
|
34
|
+
# Auto-pick a default interpreter if --python wasn't supplied
|
|
35
|
+
if python_path is None:
|
|
36
|
+
default_env = await envs.pick_default(cwd=Path.cwd())
|
|
37
|
+
if default_env is not None:
|
|
38
|
+
python_path = default_env.path
|
|
39
|
+
if not default_env.has_ipykernel:
|
|
40
|
+
print(f"Installing ipykernel into {default_env.name}…")
|
|
41
|
+
ok = False
|
|
42
|
+
async for kind, line in envs.install_ipykernel(default_env.path):
|
|
43
|
+
if kind == "done":
|
|
44
|
+
ok = line == "ok"
|
|
45
|
+
if not ok:
|
|
46
|
+
print(f" WARNING: {line}")
|
|
47
|
+
else:
|
|
48
|
+
print(f" {line}")
|
|
49
|
+
if ok:
|
|
50
|
+
await envs.refresh()
|
|
51
|
+
else:
|
|
52
|
+
print(" Kernel will start, but ipykernel import may fail.")
|
|
53
|
+
print(f"Using kernel: {default_env.name} ({default_env.path})")
|
|
54
|
+
|
|
55
|
+
pool = KernelPool(default_python_path=python_path)
|
|
56
|
+
relay: Relay[str] = Relay()
|
|
57
|
+
|
|
58
|
+
static_dir = Path(__file__).parent / "static"
|
|
59
|
+
app.assets("/static", static_dir, name="static")
|
|
60
|
+
|
|
61
|
+
app.mount("/api", api_router(db, pool, relay))
|
|
62
|
+
app.mount("/", app_router(db, pool, relay))
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
yield
|
|
66
|
+
finally:
|
|
67
|
+
await pool.shutdown_all()
|
|
68
|
+
await db.close()
|
|
69
|
+
|
|
70
|
+
return bootstrap
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_server(
|
|
74
|
+
host: str = "127.0.0.1",
|
|
75
|
+
port: int = 8080,
|
|
76
|
+
db_path: Path = Path("nb.db"),
|
|
77
|
+
python_path: str | None = None,
|
|
78
|
+
mcp_host: str | None = None,
|
|
79
|
+
mcp_port: int = 8081,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Start the grokbook server, optionally with MCP server. Blocks until Ctrl+C."""
|
|
82
|
+
|
|
83
|
+
async def _run() -> None:
|
|
84
|
+
server = Server(
|
|
85
|
+
make_bootstrap(db_path, python_path),
|
|
86
|
+
RichTracer(),
|
|
87
|
+
host=host,
|
|
88
|
+
port=port,
|
|
89
|
+
compression=CompressionConfig(zstd_level=-1),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if mcp_host is not None:
|
|
93
|
+
# Run both notebook server and MCP HTTP server concurrently
|
|
94
|
+
os.environ["GROKBOOK_API_URL"] = f"http://127.0.0.1:{port}/api"
|
|
95
|
+
from grokbook.mcp_server import mcp
|
|
96
|
+
|
|
97
|
+
await asyncio.gather(
|
|
98
|
+
server.run(),
|
|
99
|
+
mcp.run_async(
|
|
100
|
+
"streamable-http",
|
|
101
|
+
host=mcp_host,
|
|
102
|
+
port=mcp_port,
|
|
103
|
+
show_banner=False,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
await server.run()
|
|
108
|
+
|
|
109
|
+
asyncio.run(_run())
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Convert ANSI escape sequences to HTML spans."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
_ANSI_RE = re.compile(r"\x1b\[([0-9;]*)m")
|
|
6
|
+
|
|
7
|
+
_FG_COLORS = {
|
|
8
|
+
30: "#6e7681", 31: "#ff7b72", 32: "#7ee787", 33: "#d29922",
|
|
9
|
+
34: "#79c0ff", 35: "#d2a8ff", 36: "#a5d6ff", 37: "#c9d1d9",
|
|
10
|
+
90: "#8b949e", 91: "#ffa198", 92: "#9be9a8", 93: "#e3b341",
|
|
11
|
+
94: "#a5d6ff", 95: "#d2a8ff", 96: "#b6e3ff", 97: "#f0f6fc",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_BG_COLORS = {
|
|
15
|
+
40: "#6e7681", 41: "#ff7b72", 42: "#7ee787", 43: "#d29922",
|
|
16
|
+
44: "#79c0ff", 45: "#d2a8ff", 46: "#a5d6ff", 47: "#c9d1d9",
|
|
17
|
+
100: "#8b949e", 101: "#ffa198", 102: "#9be9a8", 103: "#e3b341",
|
|
18
|
+
104: "#a5d6ff", 105: "#d2a8ff", 106: "#b6e3ff", 107: "#f0f6fc",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def ansi_to_html(text: str) -> str:
|
|
23
|
+
"""Convert ANSI escape sequences in text to HTML with inline styles.
|
|
24
|
+
|
|
25
|
+
Returns HTML string with <span style="..."> tags. The output is
|
|
26
|
+
intended to be wrapped in SafeString() for rendering.
|
|
27
|
+
All non-ANSI text is HTML-escaped.
|
|
28
|
+
"""
|
|
29
|
+
import html as _html
|
|
30
|
+
|
|
31
|
+
result = []
|
|
32
|
+
pos = 0
|
|
33
|
+
span_open = False
|
|
34
|
+
current_styles: dict[str, str] = {}
|
|
35
|
+
|
|
36
|
+
for match in _ANSI_RE.finditer(text):
|
|
37
|
+
# Append text before this escape sequence (HTML-escaped)
|
|
38
|
+
if match.start() > pos:
|
|
39
|
+
result.append(_html.escape(text[pos:match.start()]))
|
|
40
|
+
pos = match.end()
|
|
41
|
+
|
|
42
|
+
codes_str = match.group(1)
|
|
43
|
+
if not codes_str:
|
|
44
|
+
codes = [0]
|
|
45
|
+
else:
|
|
46
|
+
codes = [int(c) for c in codes_str.split(";") if c]
|
|
47
|
+
|
|
48
|
+
for code in codes:
|
|
49
|
+
if code == 0:
|
|
50
|
+
current_styles.clear()
|
|
51
|
+
elif code == 1:
|
|
52
|
+
current_styles["font-weight"] = "bold"
|
|
53
|
+
elif code == 3:
|
|
54
|
+
current_styles["font-style"] = "italic"
|
|
55
|
+
elif code == 4:
|
|
56
|
+
current_styles["text-decoration"] = "underline"
|
|
57
|
+
elif code in _FG_COLORS:
|
|
58
|
+
current_styles["color"] = _FG_COLORS[code]
|
|
59
|
+
elif code in _BG_COLORS:
|
|
60
|
+
current_styles["background-color"] = _BG_COLORS[code]
|
|
61
|
+
|
|
62
|
+
# Close previous span if open
|
|
63
|
+
if span_open:
|
|
64
|
+
result.append("</span>")
|
|
65
|
+
span_open = False
|
|
66
|
+
|
|
67
|
+
# Open new span if we have styles
|
|
68
|
+
if current_styles:
|
|
69
|
+
style = ";".join(f"{k}:{v}" for k, v in current_styles.items())
|
|
70
|
+
result.append(f'<span style="{style}">')
|
|
71
|
+
span_open = True
|
|
72
|
+
|
|
73
|
+
# Append remaining text
|
|
74
|
+
if pos < len(text):
|
|
75
|
+
result.append(_html.escape(text[pos:]))
|
|
76
|
+
|
|
77
|
+
if span_open:
|
|
78
|
+
result.append("</span>")
|
|
79
|
+
|
|
80
|
+
return "".join(result)
|