clikernel 0.2.2__tar.gz → 0.2.3__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.
- {clikernel-0.2.2 → clikernel-0.2.3}/CHANGELOG.md +10 -0
- {clikernel-0.2.2/clikernel.egg-info → clikernel-0.2.3}/PKG-INFO +4 -4
- {clikernel-0.2.2 → clikernel-0.2.3}/README.md +2 -2
- {clikernel-0.2.2 → clikernel-0.2.3}/clikernel/__init__.py +1 -1
- {clikernel-0.2.2 → clikernel-0.2.3}/clikernel/core.py +17 -9
- {clikernel-0.2.2 → clikernel-0.2.3}/clikernel/mcp.py +20 -9
- {clikernel-0.2.2 → clikernel-0.2.3}/clikernel/skill.py +8 -6
- {clikernel-0.2.2 → clikernel-0.2.3/clikernel.egg-info}/PKG-INFO +4 -4
- {clikernel-0.2.2 → clikernel-0.2.3}/clikernel.egg-info/requires.txt +1 -1
- {clikernel-0.2.2 → clikernel-0.2.3}/pyproject.toml +1 -1
- {clikernel-0.2.2 → clikernel-0.2.3}/LICENSE +0 -0
- {clikernel-0.2.2 → clikernel-0.2.3}/MANIFEST.in +0 -0
- {clikernel-0.2.2 → clikernel-0.2.3}/clikernel/_modidx.py +0 -0
- {clikernel-0.2.2 → clikernel-0.2.3}/clikernel/cli.py +0 -0
- {clikernel-0.2.2 → clikernel-0.2.3}/clikernel/stream.py +0 -0
- {clikernel-0.2.2 → clikernel-0.2.3}/clikernel.egg-info/SOURCES.txt +0 -0
- {clikernel-0.2.2 → clikernel-0.2.3}/clikernel.egg-info/dependency_links.txt +0 -0
- {clikernel-0.2.2 → clikernel-0.2.3}/clikernel.egg-info/entry_points.txt +0 -0
- {clikernel-0.2.2 → clikernel-0.2.3}/clikernel.egg-info/top_level.txt +0 -0
- {clikernel-0.2.2 → clikernel-0.2.3}/setup.cfg +0 -0
- {clikernel-0.2.2 → clikernel-0.2.3}/tests/test_stream.py +0 -0
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- do not remove -->
|
|
4
4
|
|
|
5
|
+
## 0.2.3
|
|
6
|
+
|
|
7
|
+
### New Features
|
|
8
|
+
|
|
9
|
+
- Auto-create a conversation-scoped kernel on execute when none is connected, stopped at exit or on the next connect ([#35](https://github.com/AnswerDotAI/clikernel/issues/35))
|
|
10
|
+
- Add MAXLEN constant (180) for traceback line truncation, replacing hardcoded 120 ([#34](https://github.com/AnswerDotAI/clikernel/issues/34))
|
|
11
|
+
- require shell tool over subprocess/os.system for shell commands ([#33](https://github.com/AnswerDotAI/clikernel/pull/33)), thanks to [@ncoop57](https://github.com/ncoop57)
|
|
12
|
+
- document %cd ~ expansion as the idiomatic way to change kernel directory ([#30](https://github.com/AnswerDotAI/clikernel/pull/30)), thanks to [@ncoop57](https://github.com/ncoop57)
|
|
13
|
+
|
|
14
|
+
|
|
5
15
|
## 0.2.2
|
|
6
16
|
|
|
7
17
|
### New Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clikernel
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Serve persistent Jupyter kernels to LLMs as concise text, over MCP or a plain stream protocol
|
|
5
5
|
Author: clikernel contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -12,7 +12,7 @@ Requires-Python: >=3.11
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: fastcore>=2.2.2
|
|
15
|
-
Requires-Dist: jupyasyncclient>=0.2.
|
|
15
|
+
Requires-Dist: jupyasyncclient>=0.2.2
|
|
16
16
|
Requires-Dist: mcpmini>=0.0.1
|
|
17
17
|
Requires-Dist: aidialog>=0.0.7
|
|
18
18
|
Requires-Dist: pillow
|
|
@@ -28,7 +28,7 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
`clikernel` gives an LLM agent a persistent Python workbench built from two processes. A [jupygate](https://github.com/AnswerDotAI/jupygate) server runs all the time and hosts real Jupyter kernels ([ipymini](https://github.com/AnswerDotAI/ipymini) by default); kernels live there and persist until explicitly stopped. `clikernel` itself starts and stops with each conversation: a small translator the MCP host launches, speaking MCP to the model and the Jupyter kernels API to the gateway. Outputs come back as concise text — a bare `42` for a single result, tagged sections for several, tracebacks ANSI-stripped and capped.
|
|
30
30
|
|
|
31
|
-
Because kernels outlive conversations, an agent can `connect` back to yesterday’s kernel (or the user’s live solveit kernel) and find its state intact
|
|
31
|
+
Because kernels outlive conversations, an agent can `connect` back to yesterday’s kernel (or the user’s live solveit kernel) and find its state intact. Kernels created or attached explicitly are never stopped implicitly: only `stop_kernel` ends them. The one exception is the auto kernel: an `execute` with nothing connected creates a kernel scoped to the conversation, stopped again when the conversation ends or the agent connects elsewhere. Creating a kernel runs the user’s `startup.py` and installs their `inspectors.py` cell-checking rules, delivered as source so remote kernels get the same setup as local ones.
|
|
32
32
|
|
|
33
33
|
## Install
|
|
34
34
|
|
|
@@ -50,7 +50,7 @@ Register the stdio server with your MCP host, e.g. for Claude Code:
|
|
|
50
50
|
claude mcp add clikernel -- clikernel-mcp
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
The tools mirror jupygate’s kernel API plus one composite: `connect` (create a fresh kernel — running `startup.py` and installing inspectors — or attach to an existing one by id), `execute` (run code, get concise text), `list_kernels`, `stop_kernel`, `restart`, and `interrupt`.
|
|
53
|
+
The tools mirror jupygate’s kernel API plus one composite: `connect` (create a fresh kernel — running `startup.py` and installing inspectors — or attach to an existing one by id), `execute` (run code, get concise text), `list_kernels`, `stop_kernel`, `restart`, and `interrupt`. An `execute` with no kernel connected auto-creates one, scoped to the conversation: it stops at conversation end, or when `connect` moves elsewhere. Kernels made or attached with an explicit `connect` are stopped only by `stop_kernel` — a later conversation reattaches by id and continues where the last one stopped. `$CLIKERNEL_HOST` overrides the default gateway (`http://127.0.0.1:8787`).
|
|
54
54
|
|
|
55
55
|
## Configuration
|
|
56
56
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
`clikernel` gives an LLM agent a persistent Python workbench built from two processes. A [jupygate](https://github.com/AnswerDotAI/jupygate) server runs all the time and hosts real Jupyter kernels ([ipymini](https://github.com/AnswerDotAI/ipymini) by default); kernels live there and persist until explicitly stopped. `clikernel` itself starts and stops with each conversation: a small translator the MCP host launches, speaking MCP to the model and the Jupyter kernels API to the gateway. Outputs come back as concise text — a bare `42` for a single result, tagged sections for several, tracebacks ANSI-stripped and capped.
|
|
7
7
|
|
|
8
|
-
Because kernels outlive conversations, an agent can `connect` back to yesterday’s kernel (or the user’s live solveit kernel) and find its state intact
|
|
8
|
+
Because kernels outlive conversations, an agent can `connect` back to yesterday’s kernel (or the user’s live solveit kernel) and find its state intact. Kernels created or attached explicitly are never stopped implicitly: only `stop_kernel` ends them. The one exception is the auto kernel: an `execute` with nothing connected creates a kernel scoped to the conversation, stopped again when the conversation ends or the agent connects elsewhere. Creating a kernel runs the user’s `startup.py` and installs their `inspectors.py` cell-checking rules, delivered as source so remote kernels get the same setup as local ones.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -27,7 +27,7 @@ Register the stdio server with your MCP host, e.g. for Claude Code:
|
|
|
27
27
|
claude mcp add clikernel -- clikernel-mcp
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
The tools mirror jupygate’s kernel API plus one composite: `connect` (create a fresh kernel — running `startup.py` and installing inspectors — or attach to an existing one by id), `execute` (run code, get concise text), `list_kernels`, `stop_kernel`, `restart`, and `interrupt`.
|
|
30
|
+
The tools mirror jupygate’s kernel API plus one composite: `connect` (create a fresh kernel — running `startup.py` and installing inspectors — or attach to an existing one by id), `execute` (run code, get concise text), `list_kernels`, `stop_kernel`, `restart`, and `interrupt`. An `execute` with no kernel connected auto-creates one, scoped to the conversation: it stops at conversation end, or when `connect` moves elsewhere. Kernels made or attached with an explicit `connect` are stopped only by `stop_kernel` — a later conversation reattaches by id and continues where the last one stopped. `$CLIKERNEL_HOST` overrides the default gateway (`http://127.0.0.1:8787`).
|
|
31
31
|
|
|
32
32
|
## Configuration
|
|
33
33
|
|
|
@@ -8,4 +8,4 @@ Modules:
|
|
|
8
8
|
- `clikernel.skill`: Use the persistent `clikernel` MCP session as the default workspace for any task advanced through live Python execution -- stateful inspection, file-editing workflows, debugging, experiments, API probes, data transforms, or notebook-style work. Read this before writing, running, or debugging Python code in a session with `clikernel` connected.
|
|
9
9
|
- `clikernel.stream`: Streaming JSON-lines worker protocol: nbformat-shaped output events, and a supervisor for select-based UIs."""
|
|
10
10
|
|
|
11
|
-
__version__ = "0.2.
|
|
11
|
+
__version__ = "0.2.3"
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Connect to jupygate-hosted kernels and turn execution into concise text
|
|
2
2
|
|
|
3
|
-
clikernel is the LLM side of a two-process design: jupygate runs all the time and hosts the kernels; clikernel starts and stops with each conversation and holds nothing but a pointer. This module is the whole client: gateway resolution from `gateways.toml`, the concise-text rendering contract (ANSI-stripped, capped tracebacks), delivery of `startup.py` and `inspectors.py` into freshly created kernels, and `Client` — connect (create or attach), execute, interrupt, restart, stop, list. The MCP and CLI frontends are thin faces over `Client`; nothing here creates or kills a kernel except when asked.
|
|
3
|
+
clikernel is the LLM side of a two-process design: jupygate runs all the time and hosts the kernels; clikernel starts and stops with each conversation and holds nothing but a pointer. This module is the whole client: gateway resolution from `gateways.toml`, the concise-text rendering contract (ANSI-stripped, capped tracebacks), delivery of `startup.py` and `inspectors.py` into freshly created kernels, and `Client` — connect (create or attach), execute, interrupt, restart, stop, list. The MCP and CLI frontends are thin faces over `Client`; nothing here creates or kills a kernel except when asked, and the one scoped kill is opt-in: a kernel created with `auto=True` belongs to its client, ended by that client's next `connect` or by the frontend on the way out.
|
|
4
4
|
|
|
5
5
|
Docs: https://AnswerDotAI.github.io/clikernel/core.html.md"""
|
|
6
6
|
|
|
7
7
|
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.
|
|
8
8
|
|
|
9
9
|
# %% auto #0
|
|
10
|
-
__all__ = ['DEFAULT_URL', 'STATE_LOST', 'cfg_dir', 'gateways', 'resolve', 'Client']
|
|
10
|
+
__all__ = ['DEFAULT_URL', 'MAXLEN', 'STATE_LOST', 'cfg_dir', 'gateways', 'resolve', 'Client']
|
|
11
11
|
|
|
12
12
|
# %% ../nbs/00_core.ipynb #2b3b7f4a
|
|
13
13
|
import os, tomllib
|
|
@@ -18,6 +18,7 @@ from jupyasyncclient import JupyAsyncMultiKernelManager, DeadKernelError
|
|
|
18
18
|
|
|
19
19
|
# %% ../nbs/00_core.ipynb #596419b8
|
|
20
20
|
DEFAULT_URL = 'http://127.0.0.1:8787'
|
|
21
|
+
MAXLEN = 180 # Most characters shown per displayed line
|
|
21
22
|
|
|
22
23
|
def cfg_dir():
|
|
23
24
|
"The clikernel config directory"
|
|
@@ -94,7 +95,7 @@ STATE_LOST = 'NOTE: the kernel restarted with a fresh interpreter: all session s
|
|
|
94
95
|
|
|
95
96
|
class Client:
|
|
96
97
|
"One conversation's connection: a gateway manager, and the current kernel"
|
|
97
|
-
def __init__(self, cfgdir=None): self.cfgdir,self.mgr,self.kc,self.kid = cfgdir,None,None,None
|
|
98
|
+
def __init__(self, cfgdir=None): self.cfgdir,self.mgr,self.kc,self.kid,self.auto = cfgdir,None,None,None,False
|
|
98
99
|
|
|
99
100
|
async def _use(self, mgr, kid):
|
|
100
101
|
"Point at `kid` on `mgr`, dropping any previous ws (never the kernel)"
|
|
@@ -106,8 +107,14 @@ class Client:
|
|
|
106
107
|
|
|
107
108
|
# %% ../nbs/00_core.ipynb #94497e4a
|
|
108
109
|
@patch
|
|
109
|
-
async def connect(self:Client, host='', kernel=''):
|
|
110
|
-
"Connect to a gateway (create a kernel, or attach to `kernel` by id prefix); the pointer aims at it afterwards"
|
|
110
|
+
async def connect(self:Client, host='', kernel='', auto=False):
|
|
111
|
+
"Connect to a gateway (create a kernel, or attach to `kernel` by id prefix); the pointer aims at it afterwards; `auto` marks a created kernel as client-scoped"
|
|
112
|
+
note = ''
|
|
113
|
+
if self.auto: # scoped to this client: any new connect ends it, even one already dead
|
|
114
|
+
try: await self.stop()
|
|
115
|
+
except Exception as e:
|
|
116
|
+
self.kc,self.kid,self.auto = None,None,False
|
|
117
|
+
note = f'\nnote: stopping the auto kernel failed ({e})'
|
|
111
118
|
url,tok = resolve(host, self.cfgdir)
|
|
112
119
|
mgr = JupyAsyncMultiKernelManager(url, token=tok)
|
|
113
120
|
ks = await mgr.list_kernels() # verify reachability and auth now, loudly
|
|
@@ -115,7 +122,7 @@ async def connect(self:Client, host='', kernel=''):
|
|
|
115
122
|
kid = first(k['id'] for k in ks if k['id'].startswith(kernel))
|
|
116
123
|
if not kid: raise ValueError(f'no kernel matching {kernel!r} on {url}: {[k["id"][:8] for k in ks]}')
|
|
117
124
|
await self._use(mgr, kid)
|
|
118
|
-
return f'connected to existing kernel {kid} on {url}'
|
|
125
|
+
return f'connected to existing kernel {kid} on {url}' + note
|
|
119
126
|
kw = dict(cwd=os.getcwd(), env=dict(os.environ)) if not host else {} # the default gateway is local by definition: kernels start where, and as, the conversation lives (cwd and environment - so kernel-side tools that key state to the conversation, like llmdojo's doc-state, resolve it correctly)
|
|
120
127
|
kid = await mgr.start_kernel(**kw)
|
|
121
128
|
await self._use(mgr, kid)
|
|
@@ -128,7 +135,8 @@ async def connect(self:Client, host='', kernel=''):
|
|
|
128
135
|
await self.mgr.shutdown_kernel(kid)
|
|
129
136
|
self.kc,self.kid = None,None
|
|
130
137
|
raise RuntimeError(f'inspectors.py failed to load; kernel stopped:\n{res}')
|
|
131
|
-
|
|
138
|
+
self.auto = auto
|
|
139
|
+
return f'created kernel {kid} on {url}' + (f'\n{out}' if out.strip() else '') + note
|
|
132
140
|
|
|
133
141
|
@patch
|
|
134
142
|
async def execute_outs(self:Client, code):
|
|
@@ -141,7 +149,7 @@ async def execute_outs(self:Client, code):
|
|
|
141
149
|
async def execute(self:Client, code):
|
|
142
150
|
"Run `code` in the current kernel; concise rendered text of its outputs"
|
|
143
151
|
r = await self.execute_outs(code)
|
|
144
|
-
return r if isinstance(r, str) else render_text(r, tb_maxlen=
|
|
152
|
+
return r if isinstance(r, str) else render_text(r, tb_maxlen=MAXLEN)
|
|
145
153
|
|
|
146
154
|
# %% ../nbs/00_core.ipynb #1ef42fff
|
|
147
155
|
@patch
|
|
@@ -167,7 +175,7 @@ async def stop(self:Client, kernel=''):
|
|
|
167
175
|
await self.mgr.shutdown_kernel(kid)
|
|
168
176
|
if kid == self.kid:
|
|
169
177
|
await self.kc.aclose()
|
|
170
|
-
self.kc,self.kid = None,None
|
|
178
|
+
self.kc,self.kid,self.auto = None,None,False
|
|
171
179
|
return f'stopped kernel {kid}'
|
|
172
180
|
|
|
173
181
|
@patch
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""The MCP frontend: `Client` as tools on stdio
|
|
2
2
|
|
|
3
|
-
The frontend Claude Code launches per conversation: `mk_server` closes the tools over a `Client`, and `main` (the `clikernel-mcp` console script) serves it on stdio via mcpmini. There is no instructions machinery and nothing eager — usage is taught by skill text, the server answers `initialize` instantly, and nothing happens until the
|
|
3
|
+
The frontend Claude Code launches per conversation: `mk_server` closes the tools over a `Client`, and `main` (the `clikernel-mcp` console script) serves it on stdio via mcpmini. There is no instructions machinery and nothing eager — usage is taught by skill text, the server answers `initialize` instantly, and nothing happens until the first tool call. An `execute` with no kernel connected auto-connects first, so `connect` is only needed to attach, to reach another gateway, or to make a kernel that outlives the conversation. Tool descriptions carry v1's hard-won wording.
|
|
4
4
|
|
|
5
5
|
Docs: https://AnswerDotAI.github.io/clikernel/mcp.html.md"""
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ from aidialog.dialog import Message
|
|
|
17
17
|
from aidialog.hist import output_parts, merge_media
|
|
18
18
|
from aidialog.msg_parts import PartType, data_url
|
|
19
19
|
from fastcore.nbio import render_text
|
|
20
|
-
from .core import Client
|
|
20
|
+
from .core import Client, MAXLEN
|
|
21
21
|
from . import __version__
|
|
22
22
|
|
|
23
23
|
|
|
@@ -31,21 +31,28 @@ def part2block(p):
|
|
|
31
31
|
# %% ../nbs/01_mcp.ipynb #76e2d782
|
|
32
32
|
def mk_server(c:Client):
|
|
33
33
|
"An `MCPServer` whose tools close over `c`: connect, execute, and the lifecycle verbs"
|
|
34
|
+
alock = asyncio.Lock()
|
|
34
35
|
async def connect(
|
|
35
36
|
host:str='', # Gateway: empty for the default local jupygate, a `gateways.toml` name, or a URL
|
|
36
37
|
kernel:str='', # Kernel id (or unique prefix) to attach to; empty creates a fresh kernel
|
|
37
38
|
)->str:
|
|
38
|
-
"Connect to a kernel. With `kernel`: attach to that existing kernel exactly as it is (nothing is run) - this is how a later conversation returns to live state, and how to reach a kernel someone else created. Without: create a fresh kernel, run the user's startup.py in it, and install their inspectors; the reply includes the new kernel's id (reusable in a later `connect`) and the startup output. Kernels persist until explicitly stopped: disconnecting, switching, and conversation end never kill
|
|
39
|
+
"Connect to a kernel. With `kernel`: attach to that existing kernel exactly as it is (nothing is run) - this is how a later conversation returns to live state, and how to reach a kernel someone else created. Without: create a fresh kernel, run the user's startup.py in it, and install their inspectors; the reply includes the new kernel's id (reusable in a later `connect`) and the startup output. Kernels made or attached this way persist until explicitly stopped: disconnecting, switching, and conversation end never kill them. (Connecting also immediately stops the auto kernel, if `execute` had created one.)"
|
|
39
40
|
return await c.connect(host, kernel)
|
|
40
41
|
|
|
41
42
|
async def execute(
|
|
42
43
|
code:str, # Python/IPython code to run
|
|
43
44
|
):
|
|
44
|
-
"Run `code` in the current kernel, keeping state across calls (imports, variables, monkeypatches, cached objects).
|
|
45
|
+
"Run `code` in the current kernel, keeping state across calls (imports, variables, monkeypatches, cached objects). With no kernel connected, one is auto-created first (default gateway, startup.py and inspectors run), and its connect banner - kernel id and startup output - is prepended to the reply: read it. An auto-created kernel is scoped to the conversation: it stops when the conversation ends or when `connect` moves elsewhere; use `connect` for a kernel that should outlive the conversation. If the reply says the kernel died, `connect` again. Image outputs (plots etc.) come back as image blocks, resized to a token-friendly size, each preceded by its `<media id=...>` tag."
|
|
46
|
+
pre = ''
|
|
47
|
+
async with alock: # parallel first calls must not each create a kernel
|
|
48
|
+
if not c.kc: pre = await c.connect(auto=True) + '\n'
|
|
45
49
|
r = await c.execute_outs(code)
|
|
46
|
-
if isinstance(r, str): return r
|
|
47
|
-
res = merge_media(render_text(r, tb_maxlen=
|
|
48
|
-
|
|
50
|
+
if isinstance(r, str): return pre + r
|
|
51
|
+
res = merge_media(render_text(r, tb_maxlen=MAXLEN), output_parts(Message(msg_type='code', output=r)))
|
|
52
|
+
if isinstance(res, str): return pre + res
|
|
53
|
+
blocks = [part2block(p) for p in res]
|
|
54
|
+
if pre: blocks = [dict(type='text', text=pre)] + blocks
|
|
55
|
+
return dict(content=blocks, isError=False)
|
|
49
56
|
|
|
50
57
|
async def list_kernels(
|
|
51
58
|
host:str='', # Gateway to list; empty for the current one (or the default if not connected)
|
|
@@ -56,7 +63,7 @@ def mk_server(c:Client):
|
|
|
56
63
|
async def stop_kernel(
|
|
57
64
|
kernel:str='', # Kernel id (or unique prefix); empty stops the current kernel
|
|
58
65
|
)->str:
|
|
59
|
-
"Stop a kernel for good: its process ends and its state is gone.
|
|
66
|
+
"Stop a kernel for good: its process ends and its state is gone. Explicitly created or attached kernels are stopped only this way - do it when the user is done with a kernel, and leave kernels running that the user wants to return to. (A kernel auto-created by `execute` also stops itself when the conversation ends or when `connect` moves elsewhere.)"
|
|
60
67
|
return await c.stop(kernel)
|
|
61
68
|
|
|
62
69
|
async def restart()->str:
|
|
@@ -76,6 +83,10 @@ def main():
|
|
|
76
83
|
async def _main():
|
|
77
84
|
c = Client()
|
|
78
85
|
try: await serve_stdio(mk_server(c))
|
|
79
|
-
finally:
|
|
86
|
+
finally:
|
|
87
|
+
if c.auto: # an execute-made kernel is conversation-scoped; the gateway may already be gone
|
|
88
|
+
try: await c.stop()
|
|
89
|
+
except Exception as e: print(f'auto kernel stop failed: {e!r}', file=sys.stderr)
|
|
90
|
+
await c.aclose()
|
|
80
91
|
asyncio.run(_main())
|
|
81
92
|
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
# Core idea
|
|
4
4
|
|
|
5
|
-
clikernel connects this conversation to Jupyter kernels hosted by a jupygate server that runs all the time, independently of any conversation.
|
|
5
|
+
clikernel connects this conversation to Jupyter kernels hosted by a jupygate server that runs all the time, independently of any conversation. State lasts the whole conversation -- imports, live objects, monkeypatches, cached results -- and a kernel you `connect` explicitly persists across conversations too. Treat it as a notebook-style workbench, not a one-shot script runner.
|
|
6
6
|
|
|
7
|
-
Prefer it over one-off Python scripts (`python -c`, shell heredocs) whenever you need to inspect runtime behavior, test an idea, call a Python API, examine package state, run a live probe, or iterate on an implementation detail. Prefer in-kernel tools over shell equivalents when they exist: file search and directory listing go through the `rgapi` pyskill (`rg()`/`fd()`/`ls()`), and GitHub work through the `ghapi` pyskill, when those are installed. Shell commands remain the right tool for
|
|
7
|
+
Prefer it over one-off Python scripts (`python -c`, shell heredocs) whenever you need to inspect runtime behavior, test an idea, call a Python API, examine package state, run a live probe, or iterate on an implementation detail. Prefer in-kernel tools over shell equivalents when they exist: file search and directory listing go through the `rgapi` pyskill (`rg()`/`fd()`/`ls()`), and GitHub and local git work through the `ghapi` pyskill, when those are installed. Shell commands remain the right tool for project test/build commands and non-Python tools. Run them through the harness's shell tool, never `subprocess`/`os.system` from the kernel, which would bypass the harness's permission hooks.
|
|
8
8
|
|
|
9
9
|
# The lifecycle contract
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The lifecycle has one implicit convenience and no implicit destruction beyond it. An `execute` with no kernel connected auto-creates one (default gateway, `startup.py` and inspectors as usual), and the connect banner arrives prepended to that first reply. That auto kernel is scoped to the conversation: it stops when the conversation ends, or the moment `connect` moves anywhere else. Kernels you `connect` explicitly -- created bare or attached by id -- are never stopped except by `stop_kernel`. The tools are self-documenting -- read each tool's MCP description -- and the shape of a session is:
|
|
12
12
|
|
|
13
|
-
- Start of work: `
|
|
13
|
+
- Start of work: just `execute` (the first call auto-connects; read the banner: it says what is imported and what to do next), or a bare `connect` when the kernel should outlive the conversation.
|
|
14
14
|
- Returning to earlier work (the user asks to continue where a previous conversation left off, or to use their solveit kernel): `list_kernels` to see what's running, then `connect` with the kernel id (or unique prefix). Attach runs nothing -- the kernel's live state is the point.
|
|
15
|
-
- End of work: `stop_kernel`
|
|
15
|
+
- End of work: an auto kernel stops itself with the conversation. `stop_kernel` an explicitly created kernel when it was for this task only; leave it running if the user wants to return to it, and tell the user its id so they can.
|
|
16
16
|
|
|
17
17
|
`restart` gives the current kernel a genuinely fresh interpreter under the same id (redo imports after it); `interrupt` stops a too-long `execute` while keeping state. If a reply says the kernel died, `connect` again. If `connect` fails because the gateway is unreachable, the jupygate server is not running -- report that to the user rather than working around it.
|
|
18
18
|
|
|
@@ -22,13 +22,15 @@ Remote gateways (a jupygate or solveit instance elsewhere) are the same verbs wi
|
|
|
22
22
|
|
|
23
23
|
`execute` runs IPython, so magics work as written. The `%nbrun` line magic (registered by aidialog, which the standard startup imports) runs cells from a `.ipynb` file by cell id prefix -- see `doc(dsk)` after startup for its options. It runs *in the kernel*, so its cells share session state and are checked by the session's inspectors.
|
|
24
24
|
|
|
25
|
+
`%cd` expands `~` and is the idiomatic way to change the kernel's directory: prefer it over `os.chdir`.
|
|
26
|
+
|
|
25
27
|
# Session setup
|
|
26
28
|
|
|
27
29
|
`connect` (creating) first runs `~/.config/clikernel/startup.py` (with `__file__` bound, so the file can locate its neighbors), then installs inspectors from `~/.config/clikernel/inspectors.py`. Both travel as source, so remote kernels get the same setup as local ones. Inspectors see each cell before it runs (1-arg: the AST; 2-arg: AST and raw source): a returned string prints as a note ahead of the cell's output, raising `RuleBlock` (provided in the file's namespace) blocks the cell, and inspector bugs warn and fail open. Attached kernels are taken as found: no startup, no inspectors.
|
|
28
30
|
|
|
29
31
|
# Output shape
|
|
30
32
|
|
|
31
|
-
Outputs are rendered with `fastcore.nbio.render_text`. A single non-empty output comes back as its preferred text form, e.g. `42`; multiple outputs use readable XML-ish tags (`<stdout>`, `<execute_result>`, ...) with raw, unescaped body text. Exceptions come back as one clean traceback: ANSI-stripped, over-long lines capped at
|
|
33
|
+
Outputs are rendered with `fastcore.nbio.render_text`. A single non-empty output comes back as its preferred text form, e.g. `42`; multiple outputs use readable XML-ish tags (`<stdout>`, `<execute_result>`, ...) with raw, unescaped body text. Exceptions come back as one clean traceback: ANSI-stripped, over-long lines capped at 180 characters with `File `/`Cell ` locations and the exception message always whole. `input()` fails fast in-band (`allow_stdin=False`) rather than hanging. Image outputs (plots etc.) arrive as MCP image blocks, resized to a token-friendly budget, each preceded by a `<media id=...>` text tag naming it; when the image cannot be delivered, a `<media-unavailable>` note appears in the text instead.
|
|
32
34
|
|
|
33
35
|
# Interaction rules
|
|
34
36
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clikernel
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Serve persistent Jupyter kernels to LLMs as concise text, over MCP or a plain stream protocol
|
|
5
5
|
Author: clikernel contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -12,7 +12,7 @@ Requires-Python: >=3.11
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: fastcore>=2.2.2
|
|
15
|
-
Requires-Dist: jupyasyncclient>=0.2.
|
|
15
|
+
Requires-Dist: jupyasyncclient>=0.2.2
|
|
16
16
|
Requires-Dist: mcpmini>=0.0.1
|
|
17
17
|
Requires-Dist: aidialog>=0.0.7
|
|
18
18
|
Requires-Dist: pillow
|
|
@@ -28,7 +28,7 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
`clikernel` gives an LLM agent a persistent Python workbench built from two processes. A [jupygate](https://github.com/AnswerDotAI/jupygate) server runs all the time and hosts real Jupyter kernels ([ipymini](https://github.com/AnswerDotAI/ipymini) by default); kernels live there and persist until explicitly stopped. `clikernel` itself starts and stops with each conversation: a small translator the MCP host launches, speaking MCP to the model and the Jupyter kernels API to the gateway. Outputs come back as concise text — a bare `42` for a single result, tagged sections for several, tracebacks ANSI-stripped and capped.
|
|
30
30
|
|
|
31
|
-
Because kernels outlive conversations, an agent can `connect` back to yesterday’s kernel (or the user’s live solveit kernel) and find its state intact
|
|
31
|
+
Because kernels outlive conversations, an agent can `connect` back to yesterday’s kernel (or the user’s live solveit kernel) and find its state intact. Kernels created or attached explicitly are never stopped implicitly: only `stop_kernel` ends them. The one exception is the auto kernel: an `execute` with nothing connected creates a kernel scoped to the conversation, stopped again when the conversation ends or the agent connects elsewhere. Creating a kernel runs the user’s `startup.py` and installs their `inspectors.py` cell-checking rules, delivered as source so remote kernels get the same setup as local ones.
|
|
32
32
|
|
|
33
33
|
## Install
|
|
34
34
|
|
|
@@ -50,7 +50,7 @@ Register the stdio server with your MCP host, e.g. for Claude Code:
|
|
|
50
50
|
claude mcp add clikernel -- clikernel-mcp
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
The tools mirror jupygate’s kernel API plus one composite: `connect` (create a fresh kernel — running `startup.py` and installing inspectors — or attach to an existing one by id), `execute` (run code, get concise text), `list_kernels`, `stop_kernel`, `restart`, and `interrupt`.
|
|
53
|
+
The tools mirror jupygate’s kernel API plus one composite: `connect` (create a fresh kernel — running `startup.py` and installing inspectors — or attach to an existing one by id), `execute` (run code, get concise text), `list_kernels`, `stop_kernel`, `restart`, and `interrupt`. An `execute` with no kernel connected auto-creates one, scoped to the conversation: it stops at conversation end, or when `connect` moves elsewhere. Kernels made or attached with an explicit `connect` are stopped only by `stop_kernel` — a later conversation reattaches by id and continues where the last one stopped. `$CLIKERNEL_HOST` overrides the default gateway (`http://127.0.0.1:8787`).
|
|
54
54
|
|
|
55
55
|
## Configuration
|
|
56
56
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|