clikernel 0.2.10__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.
@@ -2,6 +2,21 @@
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
+
13
+ ## 0.2.11
14
+
15
+ ### New Features
16
+
17
+ - Rewrite module and API docs for clarity, harden session cleanup with try/finally and grouped errors, negotiate MCP protocol version ([#49](https://github.com/AnswerDotAI/clikernel/issues/49))
18
+
19
+
5
20
  ## 0.2.10
6
21
 
7
22
  ### New Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clikernel
3
- Version: 0.2.10
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
@@ -14,8 +14,8 @@ License-File: LICENSE
14
14
  Requires-Dist: fastcore>=2.2.2
15
15
  Requires-Dist: jupyasyncclient>=0.2.10
16
16
  Requires-Dist: jupywire>=0.1.9
17
- Requires-Dist: mcpmini>=0.0.4
18
- Requires-Dist: rustygate>=0.1.11
17
+ Requires-Dist: mcpmini>=0.0.5
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 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.
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 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).
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
- Kernel ownership determines what happens when the conversation ends:
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
- 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.
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
- 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.
65
-
66
- `$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.
57
+ ``` python
58
+ create(kernel="ipymini")
59
+ exec(code="6*7")
60
+ ```
67
61
 
68
- Pass `--quiet`, as in `clikernel-mcp --quiet`, to omit startup output from replies. Python startup code still runs.
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.
@@ -4,8 +4,8 @@ Modules:
4
4
 
5
5
  - `clikernel.cli`: The stream-protocol frontend: the service on stdin/stdout for token-reading clients
6
6
  - `clikernel.core`: Gateway naming, startup delivery, and MCP sessions on rustygate gateways
7
- - `clikernel.mcp`: The MCP frontend: a stdio router over rustygate gateways
7
+ - `clikernel.mcp`: Route stdio MCP requests to rustygate gateways
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.10"
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: the delimiter-framed stdin/stdout protocol v1 established (documented in the README, rationale unchanged — a client that reads stdout as tokens wants no echo, a cheap ack byte, and a per-process random delimiter to read until). The protocol machinery ports from v1 verbatim; underneath, the process is now one MCP session on a gateway. Run bare it creates a kernel and stops it again on exit — whoever ran the command made that decision by running it — while `--kernel` attaches to an existing kernel and leaves it exactly as found. Ctrl-C during a long cell translates into a kernel interrupt, jupyter-console style, instead of killing the process.
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
- "Run the stream protocol on stdin/stdout: announce `info` and the session delimiter, then ack each request with '.', respond with `execute(code)`, and end each response with the delimiter"
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
- line = line.rstrip("\n")
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,20 +139,22 @@ 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('py', code=''))
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('py', code=code), loop)
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)
148
153
  try: serve_stream(execute, info=info, should_exit=lambda: stop)
149
154
  finally:
150
- run(g.aclose()) # ends the session: the kernel this run created dies with it, an attached one survives
151
- if child: child.stop()
152
- loop.call_soon_threadsafe(loop.stop)
155
+ try: run(g.aclose())
156
+ finally:
157
+ try:
158
+ if child: child.stop()
159
+ finally: loop.call_soon_threadsafe(loop.stop)
153
160
 
@@ -1,6 +1,12 @@
1
1
  """Gateway naming, startup delivery, and MCP sessions on rustygate gateways
2
2
 
3
- clikernel is the LLM side of a two-process design: a gateway ([rustygate](https://github.com/AnswerDotAI/rustygate)) hosts the kernels and serves MCP itself at `POST /mcp`; clikernel starts and stops with each conversation and routes the harness's stdio MCP to gateways. This module is the client layer: gateway naming from `gateways.toml`, the per-session kernel-creation defaults (the conversation's cwd and environment, with `startup.py` and `inspectors.py` composed into one startup source), `Gateway` — one MCP session on one gateway — and `default_gateway`, which finds the local gateway or starts an owned child that lives exactly as long as the conversation. Kernel lifecycle policy lives gateway-side: ending a session stops the kernels it created with autoclose and nothing else.
3
+ [Rustygate](https://github.com/AnswerDotAI/rustygate) hosts kernels and serves MCP requests at `POST /mcp`. Clikernel runs alongside it for the duration of a conversation. It routes the LLM client's stdio MCP requests to gateways.
4
+
5
+ This module connects to those gateways. `gateways.toml` gives them names. `session_defaults` supplies kernel startup code and, for a local gateway, the conversation's working directory and environment. It combines `startup.py` and `inspectors.py` into source that can run on another machine.
6
+
7
+ `Gateway` represents one MCP session on one gateway. `default_gateway` connects to the local gateway or starts a child process if it cannot connect. The caller must stop any returned child. The MCP router does this when the conversation ends.
8
+
9
+ Rustygate tracks the session's current kernel and which kernels it created. Ending a session stops its autoclose kernels. It leaves other kernels running. Stopping an owned gateway process stops all kernels in that process.
4
10
 
5
11
  Docs: https://AnswerDotAI.github.io/clikernel/core.html.md"""
6
12
 
@@ -21,16 +27,16 @@ from . import __version__
21
27
  DEFAULT_URL = 'http://127.0.0.1:8787'
22
28
 
23
29
  def cfg_dir():
24
- "The clikernel config directory"
30
+ "Return the clikernel configuration directory."
25
31
  return xdg_config_home()/'clikernel'
26
32
 
27
33
  def gateways(cfgdir=None):
28
- "Named gateways from `gateways.toml`: `{name: {url, token | token_env, verify}}`"
34
+ "Read named gateways as `{name: {url, token | token_env, verify}}` from `gateways.toml`."
29
35
  p = (Path(cfgdir) if cfgdir else cfg_dir())/'gateways.toml'
30
36
  return tomllib.loads(p.read_text()).get('gateways', {}) if p.exists() else {}
31
37
 
32
38
  def resolve(host='', cfgdir=None):
33
- "`(url, token, verify)` for `host`: empty = the default local gateway, a URL = itself, else a `gateways.toml` name"
39
+ "Resolve an empty host, URL, or configured gateway name to `(url, token, verify)`."
34
40
  if not host: return os.environ.get('CLIKERNEL_HOST', DEFAULT_URL), os.environ.get('CLIKERNEL_TOKEN'), True
35
41
  if '://' in host: return host, os.environ.get('CLIKERNEL_TOKEN'), True
36
42
  cfgdir = Path(cfgdir) if cfgdir else cfg_dir()
@@ -41,7 +47,7 @@ def resolve(host='', cfgdir=None):
41
47
 
42
48
  # %% ../nbs/00_core.ipynb #0d846754
43
49
  def _startup_src(src, path):
44
- "The startup file's source wrapped so `__file__` is bound to its path during the run, and absent after"
50
+ "Wrap startup source with `__file__` set to its path during execution and deleted afterwards."
45
51
  return f'''__file__ = {str(path)!r}
46
52
  try: exec(compile({src!r}, __file__, 'exec'))
47
53
  finally: del __file__'''
@@ -93,15 +99,15 @@ def _inspector_setup(src):
93
99
 
94
100
  # %% ../nbs/00_core.ipynb #1362e6ce
95
101
  def startup_src(cfgdir=None):
96
- "The composed startup source for one kernel: `startup.py` wrapped, then the inspector installer"
102
+ "Build Python defaults, user startup, and inspector setup, in that order."
97
103
  d = Path(cfgdir) if cfgdir else cfg_dir()
98
- parts = []
104
+ parts = ["get_ipython().ast_node_interactivity = 'all'"]
99
105
  if (p := d/'startup.py').exists(): parts.append(_startup_src(p.read_text(), p))
100
106
  if (p := d/'inspectors.py').exists(): parts.append(_inspector_setup(p.read_text()))
101
107
  return '\n'.join(parts)
102
108
 
103
109
  def session_defaults(cfgdir=None, quiet=False, local=True):
104
- "The `rustygate` initialize extension: startup source and quiet, plus cwd and env for a local gateway"
110
+ "Return startup and quiet settings for rustygate initialization. Local sessions also include cwd and env."
105
111
  d = dict(startup=startup_src(cfgdir), quiet=quiet)
106
112
  if local:
107
113
  d['cwd'] = os.getcwd()
@@ -110,8 +116,9 @@ def session_defaults(cfgdir=None, quiet=False, local=True):
110
116
 
111
117
  # %% ../nbs/00_core.ipynb #10ed59fc
112
118
  class Gateway:
113
- "One MCP session on one rustygate: initialize with session defaults, call tools, DELETE at close"
114
- def __init__(self,
119
+ "Open an MCP session on rustygate, call its tools, and end the session with DELETE."
120
+ def __init__(
121
+ self,
115
122
  url, # The gateway base URL, e.g. 'http://127.0.0.1:8787'
116
123
  token=None, # Gateway auth token, sent as a bearer token
117
124
  verify=True, # Verify TLS certificates?
@@ -121,41 +128,48 @@ class Gateway:
121
128
  self.tr = HTTPTransport(f"{url.rstrip('/')}/mcp", token=token, http_client=client)
122
129
 
123
130
  async def rpc(self, method, **params):
124
- "One JSON-RPC request, returning its result and raising on a protocol-level error"
131
+ "Send a JSON-RPC request and return its result. Raise `RuntimeError` for protocol errors."
125
132
  self._id += 1
126
133
  r = await self.tr.send(jreq(method, self._id, **params))
127
134
  if 'error' in r: raise RuntimeError(f"{r['error']['code']}: {r['error']['message']}")
128
135
  return r['result']
129
136
 
130
137
  async def initialize(self, defaults=None):
131
- "Open the MCP session, sending `defaults` as the `rustygate` extension; returns self"
138
+ "Open the MCP session with `defaults` in the `rustygate` extension. Return self."
132
139
  await self.tr.start()
133
140
  self.info = await self.rpc('initialize', protocolVersion='2025-11-25', capabilities={},
134
141
  clientInfo=dict(name='clikernel', version=__version__), rustygate=defaults or {})
142
+ self.tr.proto = self.info['protocolVersion']
135
143
  await self.tr.send(jreq('notifications/initialized'))
136
144
  return self
137
145
 
138
- async def tools(self): return (await self.rpc('tools/list'))['tools']
139
- async def call(self, name, **args): return await self.rpc('tools/call', name=name, arguments=args)
140
-
141
- async def text(self, name, **args):
142
- "A tool call's text blocks joined; raises on `isError`"
143
- r = await self.call(name, **args)
144
- t = ''.join(c.get('text','') for c in r['content'] if c['type'] == 'text')
145
- if r.get('isError'): raise RuntimeError(t)
146
- return t
147
-
148
- async def aclose(self):
149
- "End the MCP session — the gateway stops the kernels this session created with autoclose — and drop the connection"
150
- await self.tr.delete()
151
- await self.tr.aclose()
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()
152
166
 
153
167
  # %% ../nbs/00_core.ipynb #0d054375
154
168
  async def default_gateway(
155
169
  cfgdir=None, # Config dir for `session_defaults` (the standard one if None)
156
170
  quiet=False, # Keep startup output out of replies?
157
171
  ):
158
- "An initialized `Gateway` on the default local gateway, plus the owned child rustygate when none was running (else None)"
172
+ "Return an initialized `Gateway` and its new child process, or None if it reused a gateway. The caller must stop any child."
159
173
  url, token, verify = resolve('', cfgdir)
160
174
  d = session_defaults(cfgdir, quiet)
161
175
  try: return await Gateway(url, token, verify).initialize(d), None
@@ -1,6 +1,10 @@
1
- """The MCP frontend: a stdio router over rustygate gateways
1
+ """Route stdio MCP requests to rustygate gateways
2
2
 
3
- The frontend Claude Code launches per conversation. `Router` speaks stdio MCP to the harness and forwards to gateways: rustygate serves the tool surface itself, so the router defines no tools — it fetches the local gateway's `tools/list`, adds a `host` parameter to the kernel-selection tools, and forwards `tools/call` verbatim to the right gateway, ids intact so cancellation maps through. `main` serves a `Router` on stdio and ends every gateway session on the way out — the DELETE that stops each session's autoclose kernels, and the owned child gateway with them.
3
+ Claude Code starts `clikernel-mcp` for each conversation. Its `Router` accepts MCP messages over stdio and sends tool calls to rustygate gateways. Rustygate implements the tools. The router gets their schemas from the local gateway and adds `host` to `list_kernels`, `use_kernel`, and `create`.
4
+
5
+ Each host has a separate gateway session. The router remembers one current host for calls without an explicit `host`. It removes `host` before forwarding a call but preserves the JSON-RPC request id. Cancellation notifications also retain their original ids.
6
+
7
+ `main` runs the router and closes its gateway sessions on exit. Closing a session sends an HTTP DELETE. The gateway then stops the kernels that session created with autoclose. If the router started a child gateway, it stops that process too.
4
8
 
5
9
  Docs: https://AnswerDotAI.github.io/clikernel/mcp.html.md"""
6
10
 
@@ -23,15 +27,16 @@ HOST_PARAM = {'type': 'string', 'description': 'Gateway to target: a gateways.to
23
27
  HOSTED = ('list_kernels', 'use_kernel', 'create')
24
28
 
25
29
  class Router:
26
- "Forward the harness's stdio MCP to rustygate gateways: one `Gateway` session per host, one current"
27
- def __init__(self,
30
+ "Route stdio MCP to rustygate with one session per host and one current host."
31
+ def __init__(
32
+ self,
28
33
  cfgdir=None, # Config dir for `session_defaults` and `gateways.toml` (the standard one if None)
29
34
  quiet=False, # Keep startup output out of replies?
30
35
  ):
31
36
  self.cfgdir,self.quiet,self.sessions,self.cur,self.child = cfgdir,quiet,{},'',None
32
37
 
33
38
  async def session(self, host=''):
34
- "The initialized `Gateway` for `host`, made on first use; empty means the default local gateway"
39
+ "Return the session for `host`, initializing it on first use. Empty `host` selects the default local gateway."
35
40
  if host not in self.sessions:
36
41
  if host:
37
42
  url, token, verify = resolve(host, self.cfgdir)
@@ -39,24 +44,19 @@ class Router:
39
44
  else: self.sessions[host], self.child = await default_gateway(self.cfgdir, self.quiet)
40
45
  return self.sessions[host]
41
46
 
42
- async def aclose(self):
43
- "End every gateway session — stopping each one's autoclose kernels — then the owned child gateway"
44
- for s in self.sessions.values(): await s.aclose()
45
- if self.child: self.child.stop()
46
-
47
-
48
47
  # %% ../nbs/01_mcp.ipynb #79da6a3e
49
48
  @patch
50
49
  async def tools(self:Router):
51
- "The local gateway's tools, with `host` added to the kernel-selection tools"
50
+ "Return the local gateway's tools with `host` added to `list_kernels`, `use_kernel`, and `create`."
52
51
  ts = await (await self.session()).tools()
53
52
  for t in ts:
54
53
  if t['name'] in HOSTED: t['inputSchema'].setdefault('properties', {})['host'] = dict(HOST_PARAM)
55
54
  return ts
56
55
 
56
+ # %% ../nbs/01_mcp.ipynb #e1b057dd
57
57
  @patch
58
58
  async def dispatch(self:Router, msg, requester=None):
59
- "One JSON-RPC message from the harness: initialize and ping answered here, the tool surface forwarded"
59
+ "Answer initialization and ping locally. Forward tool calls to gateways."
60
60
  method,id = msg.get('method'), msg.get('id')
61
61
  try:
62
62
  if method == 'initialize':
@@ -78,14 +78,25 @@ async def dispatch(self:Router, msg, requester=None):
78
78
  return jerr(id, -32601, f'method not found: {method}')
79
79
  except Exception as e: return None if id is None else jerr(id, -32603, str(e))
80
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
+
81
91
  # %% ../nbs/01_mcp.ipynb #3b90f0e8
82
92
  @call_parse
83
93
  def main(
84
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
85
96
  ):
86
- "The `clikernel-mcp` console script: the router on stdio"
97
+ "Run `clikernel-mcp` as a stdio server."
87
98
  async def _main():
88
- router = Router(quiet=quiet)
99
+ router = Router(cfgdir, quiet=quiet)
89
100
  try: await serve_stdio(router)
90
101
  finally: await router.aclose()
91
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
- The first `py` creates a kernel and reports what it imported: read that banner, it says what to do next. That kernel stops when the conversation ends, as does any kernel `create` makes unless it was created with `autoclose=false`. Use `create(dlgname, autoclose=false)` when a kernel should stay running afterwards, and tell the user its id — that only helps on a gateway that itself keeps running, so mention it if the reply shows a conversation-started gateway.
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 `py` and keeps state. If a reply says the kernel is gone, the next `py` starts a fresh one.
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 `py` runs there until the next selection.
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
- For native Luau work use `lua(code=...)`, or `create(dlgname, language="luau")` for a named kernel. The first `lua` auto-starts Luau when no kernel is selected, with the same autoclose rules as Python. There is one current kernel: `py` requires Python and `lua` requires Luau, so a mismatch errors rather than switching. Use `use_kernel` or `create` to select deliberately. Omit `language` to reuse an existing binding unchanged or default a new one to Python; an explicit language must match an existing binding. A `dlgname` execution override changes only that call. These rules also apply on named remote gateways.
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 `lua(code="help()")` for the bundled guide and examples, or `lua(code='help("ex.edit_file")')` for function details.
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
- - Only the last expression in a cell displays. `print(...)` any earlier value you need to see.
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.10
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
@@ -14,8 +14,8 @@ License-File: LICENSE
14
14
  Requires-Dist: fastcore>=2.2.2
15
15
  Requires-Dist: jupyasyncclient>=0.2.10
16
16
  Requires-Dist: jupywire>=0.1.9
17
- Requires-Dist: mcpmini>=0.0.4
18
- Requires-Dist: rustygate>=0.1.11
17
+ Requires-Dist: mcpmini>=0.0.5
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 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.
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 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).
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
- Kernel ownership determines what happens when the conversation ends:
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
- 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.
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
- 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.
65
-
66
- `$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.
57
+ ``` python
58
+ create(kernel="ipymini")
59
+ exec(code="6*7")
60
+ ```
67
61
 
68
- Pass `--quiet`, as in `clikernel-mcp --quiet`, to omit startup output from replies. Python startup code still runs.
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).
@@ -1,8 +1,8 @@
1
1
  fastcore>=2.2.2
2
2
  jupyasyncclient>=0.2.10
3
3
  jupywire>=0.1.9
4
- mcpmini>=0.0.4
5
- rustygate>=0.1.11
4
+ mcpmini>=0.0.5
5
+ rustygate>=0.1.19
6
6
  httpx
7
7
 
8
8
  [dev]
@@ -19,8 +19,8 @@ dependencies = [
19
19
  "fastcore>=2.2.2",
20
20
  "jupyasyncclient>=0.2.10",
21
21
  "jupywire>=0.1.9",
22
- "mcpmini>=0.0.4",
23
- "rustygate>=0.1.11",
22
+ "mcpmini>=0.0.5",
23
+ "rustygate>=0.1.19",
24
24
  "httpx",
25
25
  ]
26
26
 
@@ -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