clikernel 0.2.11__tar.gz → 0.2.12__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.11 → clikernel-0.2.12}/CHANGELOG.md +8 -0
- {clikernel-0.2.11/clikernel.egg-info → clikernel-0.2.12}/PKG-INFO +12 -18
- clikernel-0.2.12/README.md +65 -0
- {clikernel-0.2.11 → clikernel-0.2.12}/clikernel/__init__.py +1 -1
- {clikernel-0.2.11 → clikernel-0.2.12}/clikernel/_modidx.py +1 -0
- {clikernel-0.2.11 → clikernel-0.2.12}/clikernel/cli.py +27 -22
- {clikernel-0.2.11 → clikernel-0.2.12}/clikernel/core.py +24 -17
- {clikernel-0.2.11 → clikernel-0.2.12}/clikernel/mcp.py +15 -11
- {clikernel-0.2.11 → clikernel-0.2.12}/clikernel/skill.py +10 -6
- {clikernel-0.2.11 → clikernel-0.2.12/clikernel.egg-info}/PKG-INFO +12 -18
- {clikernel-0.2.11 → clikernel-0.2.12}/clikernel.egg-info/requires.txt +1 -1
- {clikernel-0.2.11 → clikernel-0.2.12}/pyproject.toml +1 -1
- clikernel-0.2.11/README.md +0 -71
- {clikernel-0.2.11 → clikernel-0.2.12}/LICENSE +0 -0
- {clikernel-0.2.11 → clikernel-0.2.12}/MANIFEST.in +0 -0
- {clikernel-0.2.11 → clikernel-0.2.12}/clikernel/stream.py +0 -0
- {clikernel-0.2.11 → clikernel-0.2.12}/clikernel.egg-info/SOURCES.txt +0 -0
- {clikernel-0.2.11 → clikernel-0.2.12}/clikernel.egg-info/dependency_links.txt +0 -0
- {clikernel-0.2.11 → clikernel-0.2.12}/clikernel.egg-info/entry_points.txt +0 -0
- {clikernel-0.2.11 → clikernel-0.2.12}/clikernel.egg-info/top_level.txt +0 -0
- {clikernel-0.2.11 → clikernel-0.2.12}/setup.cfg +0 -0
- {clikernel-0.2.11 → clikernel-0.2.12}/tests/test_stream.py +0 -0
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- do not remove -->
|
|
4
4
|
|
|
5
|
+
## 0.2.12
|
|
6
|
+
|
|
7
|
+
### New Features
|
|
8
|
+
|
|
9
|
+
- Replace py/lua tools with explicit create(kernel=...) and exec(code=...); default Python `ast_node_interactivity` to all ([#51](https://github.com/AnswerDotAI/clikernel/issues/51))
|
|
10
|
+
- Add --cfgdir flag to clikernel-mcp ([#50](https://github.com/AnswerDotAI/clikernel/pull/50)), thanks to [@ncoop57](https://github.com/ncoop57)
|
|
11
|
+
|
|
12
|
+
|
|
5
13
|
## 0.2.11
|
|
6
14
|
|
|
7
15
|
### New Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clikernel
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
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
|
|
@@ -15,7 +15,7 @@ Requires-Dist: fastcore>=2.2.2
|
|
|
15
15
|
Requires-Dist: jupyasyncclient>=0.2.10
|
|
16
16
|
Requires-Dist: jupywire>=0.1.9
|
|
17
17
|
Requires-Dist: mcpmini>=0.0.5
|
|
18
|
-
Requires-Dist: rustygate>=0.1.
|
|
18
|
+
Requires-Dist: rustygate>=0.1.19
|
|
19
19
|
Requires-Dist: httpx
|
|
20
20
|
Provides-Extra: dev
|
|
21
21
|
Requires-Dist: fastship; extra == "dev"
|
|
@@ -26,16 +26,11 @@ Dynamic: license-file
|
|
|
26
26
|
|
|
27
27
|
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
|
28
28
|
|
|
29
|
-
`clikernel` gives
|
|
29
|
+
`clikernel` gives LLM agents persistent kernel sessions over MCP or a plain stream protocol. Imports, variables, and results remain available between calls. Agents can create kernels or attach to existing ones, including a user’s live solveit kernel.
|
|
30
30
|
|
|
31
|
-
[rustygate](https://github.com/AnswerDotAI/rustygate) hosts the
|
|
31
|
+
[rustygate](https://github.com/AnswerDotAI/rustygate) hosts the kernels. Clikernel routes requests to local or named remote gateways and supplies startup code to Python kernels.
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- A session closes kernels it created with `autoclose`, including the automatic kernel used by bare `py` or `lua` and kernels created with `create`’s default settings.
|
|
36
|
-
- Attaching with `use_kernel` does not make that session responsible for closing the kernel.
|
|
37
|
-
- A kernel created with `autoclose=false` on a resident gateway can outlive the conversation. A later conversation can attach and continue using its state.
|
|
38
|
-
- A private child gateway ends with its conversation. Use a resident gateway for kernels that need to survive across conversations.
|
|
33
|
+
New kernels close with their creating conversation by default. Attaching to an existing kernel does not claim ownership. Use `autoclose=false` on a persistent gateway to keep a new kernel for later conversations. See [core](https://AnswerDotAI.github.io/clikernel/core.html) for configuration and lifecycle details.
|
|
39
34
|
|
|
40
35
|
## Install
|
|
41
36
|
|
|
@@ -57,19 +52,18 @@ Register the stdio server with your MCP host. For Claude Code:
|
|
|
57
52
|
claude mcp add clikernel -- clikernel-mcp
|
|
58
53
|
```
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
For a named kernel, use `create(dlgname="work", language="luau")`. Omit `language` to reuse an existing binding unchanged, or default a new kernel to Python. An explicit language must match an existing binding. The other tools are `list_kernels`, `use_kernel`, `delete_kernel`, `restart`, and `interrupt`; creation, selection, and listing report the language.
|
|
55
|
+
Choose a kernel, then execute code:
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
``` python
|
|
58
|
+
create(kernel="ipymini")
|
|
59
|
+
exec(code="6*7")
|
|
60
|
+
```
|
|
67
61
|
|
|
68
|
-
|
|
62
|
+
Use `kernel="luau"` for bundled Luau, or `kernel="miniapl"` for APL when miniapl is installed on the gateway. `exec` never creates or switches kernels. See [MCP](https://AnswerDotAI.github.io/clikernel/mcp.html) for named gateways, dialog bindings, and other tools.
|
|
69
63
|
|
|
70
64
|
## Configuration
|
|
71
65
|
|
|
72
|
-
Three optional files in `$XDG_CONFIG_HOME/clikernel/` configure the router, usually under `~/.config/clikernel
|
|
66
|
+
Three optional files in `$XDG_CONFIG_HOME/clikernel/` configure the router, usually under `~/.config/clikernel/`, or under the directory `clikernel-mcp --cfgdir` names:
|
|
73
67
|
|
|
74
68
|
- `startup.py` runs in each Python kernel clikernel creates, with `__file__` set to its path. Its output appears in the reply announcing the kernel unless `--quiet` is set.
|
|
75
69
|
- `inspectors.py` installs Python cell inspectors after startup. Define `inspect`, a list named `inspectors`, or both. Each inspector runs once before a cell: a one-argument inspector takes the cell’s AST, and a two-argument inspector takes the AST and raw source. Return a string to print a note before the output. Raise the provided `RuleBlock` to block execution. Other exceptions produce a warning and allow the cell to run. See [examples/inspectors.py](examples/inspectors.py).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# clikernel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
|
5
|
+
|
|
6
|
+
`clikernel` gives LLM agents persistent kernel sessions over MCP or a plain stream protocol. Imports, variables, and results remain available between calls. Agents can create kernels or attach to existing ones, including a user’s live solveit kernel.
|
|
7
|
+
|
|
8
|
+
[rustygate](https://github.com/AnswerDotAI/rustygate) hosts the kernels. Clikernel routes requests to local or named remote gateways and supplies startup code to Python kernels.
|
|
9
|
+
|
|
10
|
+
New kernels close with their creating conversation by default. Attaching to an existing kernel does not claim ownership. Use `autoclose=false` on a persistent gateway to keep a new kernel for later conversations. See [core](https://AnswerDotAI.github.io/clikernel/core.html) for configuration and lifecycle details.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
``` sh
|
|
15
|
+
pip install clikernel
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This installs rustygate and ipymini. No service setup is required for a conversation-local kernel: clikernel starts a private gateway if it cannot find one. To retain kernels across conversations, run a resident gateway, for example through launchd or systemd:
|
|
19
|
+
|
|
20
|
+
``` sh
|
|
21
|
+
rustygate --port 8787
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Use with an MCP host
|
|
25
|
+
|
|
26
|
+
Register the stdio server with your MCP host. For Claude Code:
|
|
27
|
+
|
|
28
|
+
``` sh
|
|
29
|
+
claude mcp add clikernel -- clikernel-mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Choose a kernel, then execute code:
|
|
33
|
+
|
|
34
|
+
``` python
|
|
35
|
+
create(kernel="ipymini")
|
|
36
|
+
exec(code="6*7")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use `kernel="luau"` for bundled Luau, or `kernel="miniapl"` for APL when miniapl is installed on the gateway. `exec` never creates or switches kernels. See [MCP](https://AnswerDotAI.github.io/clikernel/mcp.html) for named gateways, dialog bindings, and other tools.
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
Three optional files in `$XDG_CONFIG_HOME/clikernel/` configure the router, usually under `~/.config/clikernel/`, or under the directory `clikernel-mcp --cfgdir` names:
|
|
44
|
+
|
|
45
|
+
- `startup.py` runs in each Python kernel clikernel creates, with `__file__` set to its path. Its output appears in the reply announcing the kernel unless `--quiet` is set.
|
|
46
|
+
- `inspectors.py` installs Python cell inspectors after startup. Define `inspect`, a list named `inspectors`, or both. Each inspector runs once before a cell: a one-argument inspector takes the cell’s AST, and a two-argument inspector takes the AST and raw source. Return a string to print a note before the output. Raise the provided `RuleBlock` to block execution. Other exceptions produce a warning and allow the cell to run. See [examples/inspectors.py](examples/inspectors.py).
|
|
47
|
+
- `gateways.toml` names remote gateways and configures authentication without putting tokens in tool arguments:
|
|
48
|
+
|
|
49
|
+
``` toml
|
|
50
|
+
[gateways.solveit]
|
|
51
|
+
url = "https://solveit.example.com/gate"
|
|
52
|
+
token_env = "SOLVEIT_TOKEN"
|
|
53
|
+
verify = false # optional: accept a self-signed certificate
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## The stream protocol
|
|
57
|
+
|
|
58
|
+
Run `clikernel` as a plain CLI process for clients that read a text stream rather than MCP messages. It uses a delimiter-framed stdin/stdout protocol:
|
|
59
|
+
|
|
60
|
+
- Input is not echoed.
|
|
61
|
+
- Each request gets a `.` acknowledgement.
|
|
62
|
+
- A per-process random delimiter marks the end of each response.
|
|
63
|
+
- Multiline cells are framed by `--` and the delimiter.
|
|
64
|
+
|
|
65
|
+
The startup banner supplies the protocol instructions and delimiter. Running `clikernel` without arguments creates a kernel and stops it on exit. `--kernel <id>` attaches to an existing kernel and leaves it running on exit.
|
|
@@ -8,4 +8,4 @@ Modules:
|
|
|
8
8
|
- `clikernel.skill`: Use the `clikernel` MCP session as the default workspace for Python work: reading and changing files, notebook work, trying things out, checking how a library behaves, and reshaping data. One session stays open, so imports and variables carry between calls. 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.12"
|
|
@@ -8,6 +8,7 @@ d = { 'settings': { 'branch': 'main',
|
|
|
8
8
|
'syms': { 'clikernel.cli': { 'clikernel.cli._new_delim': ('cli.html#_new_delim', 'clikernel/cli.py'),
|
|
9
9
|
'clikernel.cli._next_line': ('cli.html#_next_line', 'clikernel/cli.py'),
|
|
10
10
|
'clikernel.cli._read_block': ('cli.html#_read_block', 'clikernel/cli.py'),
|
|
11
|
+
'clikernel.cli._response': ('cli.html#_response', 'clikernel/cli.py'),
|
|
11
12
|
'clikernel.cli._restore_termios': ('cli.html#_restore_termios', 'clikernel/cli.py'),
|
|
12
13
|
'clikernel.cli._tty_clear': ('cli.html#_tty_clear', 'clikernel/cli.py'),
|
|
13
14
|
'clikernel.cli._write_response': ('cli.html#_write_response', 'clikernel/cli.py'),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""The stream-protocol frontend: the service on stdin/stdout for token-reading clients
|
|
2
2
|
|
|
3
|
-
The `clikernel` command
|
|
3
|
+
The `clikernel` command connects a text-stream client to one gateway kernel. Requests and replies use a per-process delimiter. Input is not echoed. A `.` line acknowledges each accepted request before its result arrives.
|
|
4
|
+
|
|
5
|
+
Running the command without arguments creates a Python kernel and stops it on exit. `--kernel` attaches to an existing kernel and leaves it running. Ctrl-C during execution interrupts the kernel without ending the CLI process.
|
|
4
6
|
|
|
5
7
|
Docs: https://AnswerDotAI.github.io/clikernel/cli.html.md"""
|
|
6
8
|
|
|
@@ -38,6 +40,23 @@ def fmt_error(tag, text):
|
|
|
38
40
|
return f"<{tag}>\n{text}{nl}</{tag}>"
|
|
39
41
|
|
|
40
42
|
|
|
43
|
+
# %% ../nbs/02_cli.ipynb #5649e04a
|
|
44
|
+
def _response(line, stdin, delim, execute):
|
|
45
|
+
"Return (body, accepted), acknowledging accepted requests before execution."
|
|
46
|
+
line = line.rstrip("\n")
|
|
47
|
+
if line == delim: return fmt_error("protocol-error", "no multiline request is open: start one with a bare `--` line"), False
|
|
48
|
+
if line == _MULTILINE:
|
|
49
|
+
code, err = _read_block(stdin, delim)
|
|
50
|
+
if err: return fmt_error("protocol-error", err), False
|
|
51
|
+
elif line.startswith('%%'):
|
|
52
|
+
return fmt_error("protocol-error",
|
|
53
|
+
f"a %% cell magic needs a multiline request; send it as (flush-left):\n --\n {line}\n <rest of cell>\n {delim}"), False
|
|
54
|
+
else: code = line
|
|
55
|
+
print(".", flush=True)
|
|
56
|
+
try: return execute(code), True
|
|
57
|
+
except BaseException: return fmt_error("internal-error", traceback.format_exc()), True
|
|
58
|
+
|
|
59
|
+
# %% ../nbs/02_cli.ipynb #8c3decee
|
|
41
60
|
def _write_response(delim, body=None):
|
|
42
61
|
if body: print(body, end='' if body.endswith('\n') else '\n', flush=True)
|
|
43
62
|
print(delim, flush=True)
|
|
@@ -51,6 +70,7 @@ def _next_line(stdin):
|
|
|
51
70
|
if stdin.isatty(): raise
|
|
52
71
|
|
|
53
72
|
|
|
73
|
+
# %% ../nbs/02_cli.ipynb #bd6301f5
|
|
54
74
|
def _tty_clear(stream, idx, mask, cc=None):
|
|
55
75
|
"Clear `mask` bits in termios field `idx` when `stream` is a TTY, with optional `cc` char overrides; returns state for `_restore_termios`"
|
|
56
76
|
if not stream.isatty(): return None
|
|
@@ -69,12 +89,13 @@ def _restore_termios(state):
|
|
|
69
89
|
if state: termios.tcsetattr(state[0], termios.TCSADRAIN, state[1])
|
|
70
90
|
|
|
71
91
|
|
|
92
|
+
# %% ../nbs/02_cli.ipynb #967cb6e3
|
|
72
93
|
def serve_stream(
|
|
73
94
|
execute, # Callable `code -> str`: run one request, returning the rendered response body
|
|
74
95
|
info="", # Server info announced between the loading lines (forwarded to mcp `instructions`)
|
|
75
96
|
should_exit=None # Callable checked after each request; truthy stops the worker
|
|
76
97
|
):
|
|
77
|
-
"
|
|
98
|
+
"Serve delimiter-framed requests on stdin/stdout."
|
|
78
99
|
# ONLCR off so protocol output stays bare LF; ECHO off (echoed input corrupts the protocol) and ICANON
|
|
79
100
|
# off (canonical mode drops bytes past MAX_CANON with BEL spam; VMIN/VTIME make non-canonical reads
|
|
80
101
|
# return per byte; ISIG stays on so ^C still interrupts)
|
|
@@ -92,25 +113,9 @@ def serve_stream(
|
|
|
92
113
|
while True:
|
|
93
114
|
line = _next_line(sys.stdin)
|
|
94
115
|
if not line: break
|
|
95
|
-
|
|
96
|
-
if line == delim:
|
|
97
|
-
_write_response(delim, fmt_error("protocol-error", "no multiline request is open: start one with a bare `--` line"))
|
|
98
|
-
continue
|
|
99
|
-
if line == _MULTILINE:
|
|
100
|
-
code, err = _read_block(sys.stdin, delim)
|
|
101
|
-
if err:
|
|
102
|
-
_write_response(delim, fmt_error("protocol-error", err))
|
|
103
|
-
continue
|
|
104
|
-
elif line.startswith('%%'):
|
|
105
|
-
_write_response(delim, fmt_error("protocol-error",
|
|
106
|
-
f"a %% cell magic needs a multiline request; send it as (flush-left):\n --\n {line}\n <rest of cell>\n {delim}"))
|
|
107
|
-
continue
|
|
108
|
-
else: code = line
|
|
109
|
-
print(".", flush=True)
|
|
110
|
-
try: body = execute(code)
|
|
111
|
-
except BaseException: body = fmt_error("internal-error", traceback.format_exc())
|
|
116
|
+
body, accepted = _response(line, sys.stdin, delim, execute)
|
|
112
117
|
_write_response(delim, body)
|
|
113
|
-
if should_exit and should_exit(): break
|
|
118
|
+
if accepted and should_exit and should_exit(): break
|
|
114
119
|
finally:
|
|
115
120
|
_restore_termios(echo_state)
|
|
116
121
|
_restore_termios(output_state)
|
|
@@ -134,14 +139,14 @@ def main(
|
|
|
134
139
|
url, token, verify = resolve(host)
|
|
135
140
|
return await Gateway(url, token, verify).initialize(session_defaults(local=False)), None
|
|
136
141
|
g, child = run(_open())
|
|
137
|
-
info = run(g.text('use_kernel', kernel=kernel) if kernel else g.text('
|
|
142
|
+
info = run(g.text('use_kernel', kernel=kernel) if kernel else g.text('create', kernel='ipymini'))
|
|
138
143
|
stop = False
|
|
139
144
|
def execute(code):
|
|
140
145
|
nonlocal stop
|
|
141
146
|
if code.strip() in _EXITS:
|
|
142
147
|
stop = True
|
|
143
148
|
return ''
|
|
144
|
-
fut = asyncio.run_coroutine_threadsafe(g.text('
|
|
149
|
+
fut = asyncio.run_coroutine_threadsafe(g.text('exec', code=code), loop)
|
|
145
150
|
while True:
|
|
146
151
|
try: return fut.result()
|
|
147
152
|
except KeyboardInterrupt: asyncio.run_coroutine_threadsafe(g.call('interrupt'), loop)
|
|
@@ -99,9 +99,9 @@ def _inspector_setup(src):
|
|
|
99
99
|
|
|
100
100
|
# %% ../nbs/00_core.ipynb #1362e6ce
|
|
101
101
|
def startup_src(cfgdir=None):
|
|
102
|
-
"
|
|
102
|
+
"Build Python defaults, user startup, and inspector setup, in that order."
|
|
103
103
|
d = Path(cfgdir) if cfgdir else cfg_dir()
|
|
104
|
-
parts = []
|
|
104
|
+
parts = ["get_ipython().ast_node_interactivity = 'all'"]
|
|
105
105
|
if (p := d/'startup.py').exists(): parts.append(_startup_src(p.read_text(), p))
|
|
106
106
|
if (p := d/'inspectors.py').exists(): parts.append(_inspector_setup(p.read_text()))
|
|
107
107
|
return '\n'.join(parts)
|
|
@@ -117,7 +117,8 @@ def session_defaults(cfgdir=None, quiet=False, local=True):
|
|
|
117
117
|
# %% ../nbs/00_core.ipynb #10ed59fc
|
|
118
118
|
class Gateway:
|
|
119
119
|
"Open an MCP session on rustygate, call its tools, and end the session with DELETE."
|
|
120
|
-
def __init__(
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
121
122
|
url, # The gateway base URL, e.g. 'http://127.0.0.1:8787'
|
|
122
123
|
token=None, # Gateway auth token, sent as a bearer token
|
|
123
124
|
verify=True, # Verify TLS certificates?
|
|
@@ -142,20 +143,26 @@ class Gateway:
|
|
|
142
143
|
await self.tr.send(jreq('notifications/initialized'))
|
|
143
144
|
return self
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
146
|
+
# %% ../nbs/00_core.ipynb #2dd701f9
|
|
147
|
+
@patch
|
|
148
|
+
async def tools(self:Gateway): return (await self.rpc('tools/list'))['tools']
|
|
149
|
+
@patch
|
|
150
|
+
async def call(self:Gateway, name, **args): return await self.rpc('tools/call', name=name, arguments=args)
|
|
151
|
+
|
|
152
|
+
@patch
|
|
153
|
+
async def text(self:Gateway, name, **args):
|
|
154
|
+
"Join a tool reply's text blocks. Raise `RuntimeError` for `isError` replies."
|
|
155
|
+
r = await self.call(name, **args)
|
|
156
|
+
t = ''.join(c.get('text','') for c in r['content'] if c['type'] == 'text')
|
|
157
|
+
if r.get('isError'): raise RuntimeError(t)
|
|
158
|
+
return t
|
|
159
|
+
|
|
160
|
+
# %% ../nbs/00_core.ipynb #dac7dd25
|
|
161
|
+
@patch
|
|
162
|
+
async def aclose(self:Gateway):
|
|
163
|
+
"Request session termination and close the HTTP connection."
|
|
164
|
+
try: await self.tr.delete()
|
|
165
|
+
finally: await self.tr.aclose()
|
|
159
166
|
|
|
160
167
|
# %% ../nbs/00_core.ipynb #0d054375
|
|
161
168
|
async def default_gateway(
|
|
@@ -28,7 +28,8 @@ HOSTED = ('list_kernels', 'use_kernel', 'create')
|
|
|
28
28
|
|
|
29
29
|
class Router:
|
|
30
30
|
"Route stdio MCP to rustygate with one session per host and one current host."
|
|
31
|
-
def __init__(
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
32
33
|
cfgdir=None, # Config dir for `session_defaults` and `gateways.toml` (the standard one if None)
|
|
33
34
|
quiet=False, # Keep startup output out of replies?
|
|
34
35
|
):
|
|
@@ -43,15 +44,6 @@ class Router:
|
|
|
43
44
|
else: self.sessions[host], self.child = await default_gateway(self.cfgdir, self.quiet)
|
|
44
45
|
return self.sessions[host]
|
|
45
46
|
|
|
46
|
-
async def aclose(self):
|
|
47
|
-
"Close all sessions and their autoclose kernels, then stop any owned gateway."
|
|
48
|
-
try: results = await asyncio.gather(*(s.aclose() for s in self.sessions.values()), return_exceptions=True)
|
|
49
|
-
finally:
|
|
50
|
-
if self.child: self.child.stop()
|
|
51
|
-
errors = [r for r in results if isinstance(r, BaseException)]
|
|
52
|
-
if errors: raise BaseExceptionGroup('MCP session cleanup failed', errors)
|
|
53
|
-
|
|
54
|
-
|
|
55
47
|
# %% ../nbs/01_mcp.ipynb #79da6a3e
|
|
56
48
|
@patch
|
|
57
49
|
async def tools(self:Router):
|
|
@@ -61,6 +53,7 @@ async def tools(self:Router):
|
|
|
61
53
|
if t['name'] in HOSTED: t['inputSchema'].setdefault('properties', {})['host'] = dict(HOST_PARAM)
|
|
62
54
|
return ts
|
|
63
55
|
|
|
56
|
+
# %% ../nbs/01_mcp.ipynb #e1b057dd
|
|
64
57
|
@patch
|
|
65
58
|
async def dispatch(self:Router, msg, requester=None):
|
|
66
59
|
"Answer initialization and ping locally. Forward tool calls to gateways."
|
|
@@ -85,14 +78,25 @@ async def dispatch(self:Router, msg, requester=None):
|
|
|
85
78
|
return jerr(id, -32601, f'method not found: {method}')
|
|
86
79
|
except Exception as e: return None if id is None else jerr(id, -32603, str(e))
|
|
87
80
|
|
|
81
|
+
# %% ../nbs/01_mcp.ipynb #f72f6253
|
|
82
|
+
@patch
|
|
83
|
+
async def aclose(self:Router):
|
|
84
|
+
"Close all sessions and their autoclose kernels, then stop any owned gateway."
|
|
85
|
+
try: results = await asyncio.gather(*(s.aclose() for s in self.sessions.values()), return_exceptions=True)
|
|
86
|
+
finally:
|
|
87
|
+
if self.child: self.child.stop()
|
|
88
|
+
errors = [r for r in results if isinstance(r, BaseException)]
|
|
89
|
+
if errors: raise BaseExceptionGroup('MCP session cleanup failed', errors)
|
|
90
|
+
|
|
88
91
|
# %% ../nbs/01_mcp.ipynb #3b90f0e8
|
|
89
92
|
@call_parse
|
|
90
93
|
def main(
|
|
91
94
|
quiet:store_true=False, # Keep startup output out of replies
|
|
95
|
+
cfgdir:str=None, # Directory holding `startup.py`, `inspectors.py`, and `gateways.toml`; `~/.config/clikernel/` if unset
|
|
92
96
|
):
|
|
93
97
|
"Run `clikernel-mcp` as a stdio server."
|
|
94
98
|
async def _main():
|
|
95
|
-
router = Router(quiet=quiet)
|
|
99
|
+
router = Router(cfgdir, quiet=quiet)
|
|
96
100
|
try: await serve_stdio(router)
|
|
97
101
|
finally: await router.aclose()
|
|
98
102
|
asyncio.run(_main())
|
|
@@ -4,24 +4,28 @@ Prefer it over one-off Python scripts (`python -c`, shell heredocs). Prefer in-k
|
|
|
4
4
|
|
|
5
5
|
# Starting and stopping
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Call `create(kernel="ipymini")` before executing Python code. Creation reports what startup imported: read that banner, it says what to do next. New kernels require an explicit implementation. Add `dlgname` to bind a kernel to a dialog. Omit `kernel` only to reuse an existing binding. An explicit implementation must match that binding.
|
|
8
|
+
|
|
9
|
+
A kernel this conversation creates stops when the conversation ends, unless created with `autoclose=false`. Use `create(kernel="ipymini", dlgname="work", autoclose=false)` when a kernel should stay running afterwards, and tell the user its id. This requires a gateway that keeps running. Mention it if the reply shows a conversation-started gateway.
|
|
8
10
|
|
|
9
11
|
To continue earlier work, or to use the user's solveit kernel, `list_kernels` then `use_kernel` with the id. Attaching runs no setup and claims no ownership: the kernel's live state is the point, and it is never stopped for you.
|
|
10
12
|
|
|
11
|
-
`restart` gives a fresh interpreter under the same id; startup re-runs in kernels this conversation created, so redo everything else. `interrupt` stops a long `
|
|
13
|
+
`restart` gives a fresh interpreter under the same id; Python startup re-runs in Python kernels this conversation created, so redo everything else. `interrupt` stops a long `exec` and keeps state. If a reply says the kernel is gone, call `create` or `use_kernel` before executing again. Execution never creates or switches kernels.
|
|
12
14
|
|
|
13
|
-
Remote gateways are the same tools with a `host` argument on `list_kernels`, `use_kernel`, and `create`, named in `~/.config/clikernel/gateways.toml`. After selecting with a host, plain `
|
|
15
|
+
Remote gateways are the same tools with a `host` argument on `list_kernels`, `use_kernel`, and `create`, named in `~/.config/clikernel/gateways.toml`. After selecting with a host, plain `exec` runs there until the next selection.
|
|
14
16
|
|
|
15
17
|
# Working in it
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
Run code with `exec(code=...)` in the selected kernel. For native Luau work, select `create(kernel="luau")` first. The same execution tool and autoclose rules apply to every implementation. A `dlgname` execution override uses an existing binding for that call without changing the current selection. These rules also apply on named remote gateways.
|
|
20
|
+
|
|
21
|
+
For APL, use `create(kernel="miniapl")` then `exec(code="avg←+/÷≢ ⋄ avg 2 4 9")`. Miniapl must be installed on the gateway's PATH. Python startup and inspectors do not run in APL.
|
|
18
22
|
|
|
19
|
-
Start native work with `
|
|
23
|
+
Start native work with `exec(code="help()")` for the bundled guide and examples, or `exec(code='help("ex.edit_file")')` for function details.
|
|
20
24
|
|
|
21
25
|
Luau has persistent globals, cell-local `local` variables, and native `rg.search`, `rg.find`, `fs.read_text`, `ex.edit_text`, `ex.view_file`/`ex.edit_file`, `ex.view_cell`/`ex.edit_cell`, `os.execute`, and `io.popen` APIs. File edits use arrays of command fields (like Python exhash tuples); `{inplace=false}` previews. Shell commands use `/bin/sh -c`; pipes support read/lines/write/flush/close and persist until closed. Interrupts terminate subprocess groups and close outstanding pipes. Python startup/inspectors and IPython magics do not apply to Luau. The following guidance is for Python:
|
|
22
26
|
|
|
23
27
|
- Magics work as written, including `%%bash` for shell work. `%cd` expands `~` and is the way to change directory: prefer it over `os.chdir`.
|
|
24
|
-
-
|
|
28
|
+
- Python kernels created by clikernel display every expression result by default (`ast_node_interactivity='all'`); user startup can override this.
|
|
25
29
|
- Everything a cell outputs lands in the conversation. Be selective: `len(v)` first, then decide what to show.
|
|
26
30
|
- Don't re-run an `import` already run this session. If a name raises `NameError`, the kernel restarted or is newly attached: redo setup.
|
|
27
31
|
- After an `nbdev-export` (or any edit to a module already imported), `importlib.reload` that module and re-import any names you hold from it: a name bound by `from x import y` keeps the old object, while a `@patch`ed method refreshes with the reload because the patch writes onto the shared class. Restart only when a class you hold instances of was itself redefined. Check what is loaded by calling it, never with `inspect.getsource`, which reads the file on disk.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clikernel
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
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
|
|
@@ -15,7 +15,7 @@ Requires-Dist: fastcore>=2.2.2
|
|
|
15
15
|
Requires-Dist: jupyasyncclient>=0.2.10
|
|
16
16
|
Requires-Dist: jupywire>=0.1.9
|
|
17
17
|
Requires-Dist: mcpmini>=0.0.5
|
|
18
|
-
Requires-Dist: rustygate>=0.1.
|
|
18
|
+
Requires-Dist: rustygate>=0.1.19
|
|
19
19
|
Requires-Dist: httpx
|
|
20
20
|
Provides-Extra: dev
|
|
21
21
|
Requires-Dist: fastship; extra == "dev"
|
|
@@ -26,16 +26,11 @@ Dynamic: license-file
|
|
|
26
26
|
|
|
27
27
|
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
|
28
28
|
|
|
29
|
-
`clikernel` gives
|
|
29
|
+
`clikernel` gives LLM agents persistent kernel sessions over MCP or a plain stream protocol. Imports, variables, and results remain available between calls. Agents can create kernels or attach to existing ones, including a user’s live solveit kernel.
|
|
30
30
|
|
|
31
|
-
[rustygate](https://github.com/AnswerDotAI/rustygate) hosts the
|
|
31
|
+
[rustygate](https://github.com/AnswerDotAI/rustygate) hosts the kernels. Clikernel routes requests to local or named remote gateways and supplies startup code to Python kernels.
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- A session closes kernels it created with `autoclose`, including the automatic kernel used by bare `py` or `lua` and kernels created with `create`’s default settings.
|
|
36
|
-
- Attaching with `use_kernel` does not make that session responsible for closing the kernel.
|
|
37
|
-
- A kernel created with `autoclose=false` on a resident gateway can outlive the conversation. A later conversation can attach and continue using its state.
|
|
38
|
-
- A private child gateway ends with its conversation. Use a resident gateway for kernels that need to survive across conversations.
|
|
33
|
+
New kernels close with their creating conversation by default. Attaching to an existing kernel does not claim ownership. Use `autoclose=false` on a persistent gateway to keep a new kernel for later conversations. See [core](https://AnswerDotAI.github.io/clikernel/core.html) for configuration and lifecycle details.
|
|
39
34
|
|
|
40
35
|
## Install
|
|
41
36
|
|
|
@@ -57,19 +52,18 @@ Register the stdio server with your MCP host. For Claude Code:
|
|
|
57
52
|
claude mcp add clikernel -- clikernel-mcp
|
|
58
53
|
```
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
For a named kernel, use `create(dlgname="work", language="luau")`. Omit `language` to reuse an existing binding unchanged, or default a new kernel to Python. An explicit language must match an existing binding. The other tools are `list_kernels`, `use_kernel`, `delete_kernel`, `restart`, and `interrupt`; creation, selection, and listing report the language.
|
|
55
|
+
Choose a kernel, then execute code:
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
``` python
|
|
58
|
+
create(kernel="ipymini")
|
|
59
|
+
exec(code="6*7")
|
|
60
|
+
```
|
|
67
61
|
|
|
68
|
-
|
|
62
|
+
Use `kernel="luau"` for bundled Luau, or `kernel="miniapl"` for APL when miniapl is installed on the gateway. `exec` never creates or switches kernels. See [MCP](https://AnswerDotAI.github.io/clikernel/mcp.html) for named gateways, dialog bindings, and other tools.
|
|
69
63
|
|
|
70
64
|
## Configuration
|
|
71
65
|
|
|
72
|
-
Three optional files in `$XDG_CONFIG_HOME/clikernel/` configure the router, usually under `~/.config/clikernel
|
|
66
|
+
Three optional files in `$XDG_CONFIG_HOME/clikernel/` configure the router, usually under `~/.config/clikernel/`, or under the directory `clikernel-mcp --cfgdir` names:
|
|
73
67
|
|
|
74
68
|
- `startup.py` runs in each Python kernel clikernel creates, with `__file__` set to its path. Its output appears in the reply announcing the kernel unless `--quiet` is set.
|
|
75
69
|
- `inspectors.py` installs Python cell inspectors after startup. Define `inspect`, a list named `inspectors`, or both. Each inspector runs once before a cell: a one-argument inspector takes the cell’s AST, and a two-argument inspector takes the AST and raw source. Return a string to print a note before the output. Raise the provided `RuleBlock` to block execution. Other exceptions produce a warning and allow the cell to run. See [examples/inspectors.py](examples/inspectors.py).
|
clikernel-0.2.11/README.md
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# clikernel
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
|
|
5
|
-
|
|
6
|
-
`clikernel` gives an LLM agent a persistent Python or Luau session. Imports, variables, and results remain available between tool calls. An agent can create a kernel or attach to an existing one, including a user’s live solveit kernel, on a local or named remote gateway.
|
|
7
|
-
|
|
8
|
-
[rustygate](https://github.com/AnswerDotAI/rustygate) hosts the Jupyter kernels and provides their MCP tools. `clikernel` is the conversation-side router: the MCP host launches it over stdio, and it forwards requests to gateways over HTTP. It selects gateways from `gateways.toml`, supplies `startup.py` and `inspectors.py` to Python kernels it creates, and starts a private local gateway when needed. The default kernel is [ipymini](https://github.com/AnswerDotAI/ipymini).
|
|
9
|
-
|
|
10
|
-
Kernel ownership determines what happens when the conversation ends:
|
|
11
|
-
|
|
12
|
-
- A session closes kernels it created with `autoclose`, including the automatic kernel used by bare `py` or `lua` and kernels created with `create`’s default settings.
|
|
13
|
-
- Attaching with `use_kernel` does not make that session responsible for closing the kernel.
|
|
14
|
-
- A kernel created with `autoclose=false` on a resident gateway can outlive the conversation. A later conversation can attach and continue using its state.
|
|
15
|
-
- A private child gateway ends with its conversation. Use a resident gateway for kernels that need to survive across conversations.
|
|
16
|
-
|
|
17
|
-
## Install
|
|
18
|
-
|
|
19
|
-
``` sh
|
|
20
|
-
pip install clikernel
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
This installs rustygate and ipymini. No service setup is required for a conversation-local kernel: clikernel starts a private gateway if it cannot find one. To retain kernels across conversations, run a resident gateway, for example through launchd or systemd:
|
|
24
|
-
|
|
25
|
-
``` sh
|
|
26
|
-
rustygate --port 8787
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Use with an MCP host
|
|
30
|
-
|
|
31
|
-
Register the stdio server with your MCP host. For Claude Code:
|
|
32
|
-
|
|
33
|
-
``` sh
|
|
34
|
-
claude mcp add clikernel -- clikernel-mcp
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Use `py(code=...)` for Python/IPython or `lua(code=...)` for bundled Luau. Either starts its language’s kernel when none is current. There is one current kernel: a language mismatch errors without switching or running the code. IPython magics, including `%%bash`, are Python-only.
|
|
38
|
-
|
|
39
|
-
For a named kernel, use `create(dlgname="work", language="luau")`. Omit `language` to reuse an existing binding unchanged, or default a new kernel to Python. An explicit language must match an existing binding. The other tools are `list_kernels`, `use_kernel`, `delete_kernel`, `restart`, and `interrupt`; creation, selection, and listing report the language.
|
|
40
|
-
|
|
41
|
-
These tools forward to rustygate. `list_kernels`, `use_kernel`, and `create` also accept a `host` naming a gateway from `gateways.toml`. One MCP registration can therefore reach multiple machines. Replies retain the gateway’s text and image blocks. Python startup and inspectors run only in Python, never Luau, including after restart.
|
|
42
|
-
|
|
43
|
-
`$CLIKERNEL_HOST` overrides the default gateway URL, `http://127.0.0.1:8787`. If no gateway answers, clikernel uses a private child gateway for the conversation. The ownership rules above determine which kernels close at session end.
|
|
44
|
-
|
|
45
|
-
Pass `--quiet`, as in `clikernel-mcp --quiet`, to omit startup output from replies. Python startup code still runs.
|
|
46
|
-
|
|
47
|
-
## Configuration
|
|
48
|
-
|
|
49
|
-
Three optional files in `$XDG_CONFIG_HOME/clikernel/` configure the router, usually under `~/.config/clikernel/`:
|
|
50
|
-
|
|
51
|
-
- `startup.py` runs in each Python kernel clikernel creates, with `__file__` set to its path. Its output appears in the reply announcing the kernel unless `--quiet` is set.
|
|
52
|
-
- `inspectors.py` installs Python cell inspectors after startup. Define `inspect`, a list named `inspectors`, or both. Each inspector runs once before a cell: a one-argument inspector takes the cell’s AST, and a two-argument inspector takes the AST and raw source. Return a string to print a note before the output. Raise the provided `RuleBlock` to block execution. Other exceptions produce a warning and allow the cell to run. See [examples/inspectors.py](examples/inspectors.py).
|
|
53
|
-
- `gateways.toml` names remote gateways and configures authentication without putting tokens in tool arguments:
|
|
54
|
-
|
|
55
|
-
``` toml
|
|
56
|
-
[gateways.solveit]
|
|
57
|
-
url = "https://solveit.example.com/gate"
|
|
58
|
-
token_env = "SOLVEIT_TOKEN"
|
|
59
|
-
verify = false # optional: accept a self-signed certificate
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## The stream protocol
|
|
63
|
-
|
|
64
|
-
Run `clikernel` as a plain CLI process for clients that read a text stream rather than MCP messages. It uses a delimiter-framed stdin/stdout protocol:
|
|
65
|
-
|
|
66
|
-
- Input is not echoed.
|
|
67
|
-
- Each request gets a `.` acknowledgement.
|
|
68
|
-
- A per-process random delimiter marks the end of each response.
|
|
69
|
-
- Multiline cells are framed by `--` and the delimiter.
|
|
70
|
-
|
|
71
|
-
The startup banner supplies the protocol instructions and delimiter. Running `clikernel` without arguments creates a kernel and stops it on exit. `--kernel <id>` attaches to an existing kernel and leaves it running on exit.
|
|
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
|