jupyter-kernel-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Miroslav Hruska
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: jupyter-kernel-cli
3
+ Version: 0.1.0
4
+ Summary: Agent-friendly CLI and Python API for talking to existing Jupyter kernels.
5
+ Home-page: https://github.com/hruskamiro/jupyter-kernel-client
6
+ Author: Miroslav Hruska
7
+ Author-email: hruska.miro@gmail.com
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/hruskamiro/jupyter-kernel-client
10
+ Project-URL: Repository, https://github.com/hruskamiro/jupyter-kernel-client
11
+ Project-URL: Issues, https://github.com/hruskamiro/jupyter-kernel-client/issues
12
+ Keywords: jupyter,kernel,cli,automation,agent
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3 :: Only
25
+ Classifier: Topic :: Software Development
26
+ Classifier: Topic :: Utilities
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: jupyter-client>=7.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: build>=1.0; extra == "dev"
33
+ Requires-Dist: twine>=5.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # jupyter-kernel-client
37
+
38
+ `jk` is a small command-line tool and Python library for **executing code in an existing Jupyter kernel**.
39
+
40
+ It is designed for scripts and AI agents that need a **stable, machine-readable way to inspect or modify a live Python session**.
41
+
42
+ ## Intended Use Case
43
+
44
+ You are working in an IPython console, Spyder console, notebook kernel, or other Jupyter-backed Python session. **The session already has important state loaded:** imports, data frames, models, helper functions, configuration, intermediate results, and whatever else you have built up interactively.
45
+
46
+ **Instead of asking an AI agent to recreate that state from scratch, give it access to the existing kernel.** Export or copy the kernel connection information, tell the agent to use `jk`, and let it inspect variables, run experiments, evaluate expressions, and return structured output from the same live Python process you are using.
47
+
48
+ The workflow is:
49
+
50
+ 1. **Work normally** in an IPython, Spyder, notebook, or other Jupyter-backed console.
51
+ 2. **Load the state you care about:** data, objects, functions, imports, models, and intermediate results.
52
+ 3. **Copy the active kernel connection info**, for example with `%connect_info`.
53
+ 4. **Give that connection info to Codex or another agent.**
54
+ 5. **Tell the agent to use `jk`** to connect to that exact kernel.
55
+ 6. **Let the agent inspect and experiment** with `jk exec`, `jk eval`, `jk get`, and `jk vars`.
56
+
57
+ This is useful when the hard part is **not writing code from a blank environment**, but **exploring and manipulating the state that already exists in a live session**.
58
+
59
+ ## Install
60
+
61
+ Recommended:
62
+
63
+ ```bash
64
+ pipx install jupyter-kernel-cli
65
+ ```
66
+
67
+ Other install paths:
68
+
69
+ ```bash
70
+ pipx install git+https://github.com/hruskamiro/jupyter-kernel-client.git
71
+ pipx upgrade jupyter-kernel-cli
72
+ pipx install --force .
73
+ python -m pip install -e ".[dev]"
74
+ ```
75
+
76
+ Check:
77
+
78
+ ```bash
79
+ jk --help
80
+ ```
81
+
82
+ ## Usage
83
+
84
+ Common commands:
85
+
86
+ ```bash
87
+ jk kernels
88
+ jk kernels --probe
89
+ jk exec -f /path/to/kernel.json "x = 41"
90
+ jk eval -f /path/to/kernel.json "x + 1"
91
+ jk get -f /path/to/kernel.json x
92
+ jk vars -f /path/to/kernel.json --json
93
+ jk demo -f /path/to/kernel.json --json
94
+ ```
95
+
96
+ Use **JSON for agents** and stdin for larger generated code:
97
+
98
+ ```bash
99
+ cat <<'PY' | jk exec -f /path/to/kernel.json --json --stdin
100
+ import pandas as pd
101
+
102
+ summary = {
103
+ "variables": sorted(name for name in globals() if not name.startswith("_")),
104
+ "answer": 6 * 7,
105
+ }
106
+ summary
107
+ PY
108
+ ```
109
+
110
+ Other supported forms:
111
+
112
+ ```bash
113
+ jk exec -f /path/to/kernel.json --file script.py
114
+ jk -f /path/to/kernel.json --json eval "x + 1"
115
+ export JK_CONNECTION_FILE=/path/to/kernel.json
116
+ jk eval "df.shape"
117
+ ```
118
+
119
+ The JSON response includes status, stdout, stderr, rich display outputs, final `text/plain` result, parsed Python literal when possible, traceback details, elapsed time, timeout state, and message id.
120
+
121
+ ## Using `jk` from Codex
122
+
123
+ One working pattern is to start Codex with workspace sandboxing and on-request approvals:
124
+
125
+ ```bash
126
+ codex -s workspace-write -a on-request
127
+ ```
128
+
129
+ The exact command and permission flow may change across Codex versions, approval policies, sandbox settings, and local configuration. The important point is that Codex may need permission to run `jk` outside its command sandbox so it can open the local Jupyter kernel connection.
130
+
131
+ Give Codex the **active kernel connection information**. You can copy it from
132
+ `%connect_info`, or use
133
+ [`spyder-copy-current`](https://github.com/hruskamiro/spyder-copy-current) and
134
+ press `Ctrl+Alt+K` in Spyder to copy the current console's connection
135
+ information.
136
+
137
+ Ask Codex explicitly to **run `jk` outside its command sandbox**. For example:
138
+
139
+ ```text
140
+ Connect to this exact Jupyter kernel using jk. Run jk with an elevated request
141
+ and ask for reusable approval of the jk executable prefix.
142
+
143
+ <paste the kernel connection information here>
144
+ ```
145
+
146
+ Approve the permission request shown by Codex. If the interface offers reusable command-prefix approval, scope it to the resolved `jk` executable, which can be found with `command -v jk`.
147
+
148
+ **Approving `jk` allows arbitrary code execution in the connected Jupyter
149
+ kernel.** Treat this as execution access to that live Python session, even when
150
+ the rest of the Codex session remains workspace-sandboxed.
151
+
152
+ ## JSON Contract
153
+
154
+ Successful JSON responses include:
155
+
156
+ ```json
157
+ {
158
+ "status": "ok",
159
+ "ok": true,
160
+ "execution_count": 12,
161
+ "stdout": "",
162
+ "stderr": "",
163
+ "outputs": [],
164
+ "result_text": "42",
165
+ "result_python": 42,
166
+ "ename": null,
167
+ "evalue": null,
168
+ "traceback": [],
169
+ "elapsed_seconds": 0.01,
170
+ "timed_out": false,
171
+ "msg_id": "..."
172
+ }
173
+ ```
174
+
175
+ Exit codes:
176
+
177
+ ```text
178
+ 0 kernel execution succeeded
179
+ 1 kernel execution raised an error
180
+ 2 client or argument error
181
+ 124 client timed out waiting for the kernel
182
+ ```
183
+
184
+ Timeouts only stop the client wait. With only a connection file, `jk` cannot reliably kill or interrupt an arbitrary kernel process, so timed-out execution may continue in the kernel.
185
+
186
+ ## Python API
187
+
188
+ ```python
189
+ from jupyter_kernel_client import execute, eval_expression, get_variable
190
+
191
+ response = eval_expression("/path/to/kernel.json", "x + 1")
192
+ if response.ok:
193
+ print(response.result_python)
194
+ ```
195
+
196
+ ## License
197
+
198
+ MIT.
@@ -0,0 +1,163 @@
1
+ # jupyter-kernel-client
2
+
3
+ `jk` is a small command-line tool and Python library for **executing code in an existing Jupyter kernel**.
4
+
5
+ It is designed for scripts and AI agents that need a **stable, machine-readable way to inspect or modify a live Python session**.
6
+
7
+ ## Intended Use Case
8
+
9
+ You are working in an IPython console, Spyder console, notebook kernel, or other Jupyter-backed Python session. **The session already has important state loaded:** imports, data frames, models, helper functions, configuration, intermediate results, and whatever else you have built up interactively.
10
+
11
+ **Instead of asking an AI agent to recreate that state from scratch, give it access to the existing kernel.** Export or copy the kernel connection information, tell the agent to use `jk`, and let it inspect variables, run experiments, evaluate expressions, and return structured output from the same live Python process you are using.
12
+
13
+ The workflow is:
14
+
15
+ 1. **Work normally** in an IPython, Spyder, notebook, or other Jupyter-backed console.
16
+ 2. **Load the state you care about:** data, objects, functions, imports, models, and intermediate results.
17
+ 3. **Copy the active kernel connection info**, for example with `%connect_info`.
18
+ 4. **Give that connection info to Codex or another agent.**
19
+ 5. **Tell the agent to use `jk`** to connect to that exact kernel.
20
+ 6. **Let the agent inspect and experiment** with `jk exec`, `jk eval`, `jk get`, and `jk vars`.
21
+
22
+ This is useful when the hard part is **not writing code from a blank environment**, but **exploring and manipulating the state that already exists in a live session**.
23
+
24
+ ## Install
25
+
26
+ Recommended:
27
+
28
+ ```bash
29
+ pipx install jupyter-kernel-cli
30
+ ```
31
+
32
+ Other install paths:
33
+
34
+ ```bash
35
+ pipx install git+https://github.com/hruskamiro/jupyter-kernel-client.git
36
+ pipx upgrade jupyter-kernel-cli
37
+ pipx install --force .
38
+ python -m pip install -e ".[dev]"
39
+ ```
40
+
41
+ Check:
42
+
43
+ ```bash
44
+ jk --help
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ Common commands:
50
+
51
+ ```bash
52
+ jk kernels
53
+ jk kernels --probe
54
+ jk exec -f /path/to/kernel.json "x = 41"
55
+ jk eval -f /path/to/kernel.json "x + 1"
56
+ jk get -f /path/to/kernel.json x
57
+ jk vars -f /path/to/kernel.json --json
58
+ jk demo -f /path/to/kernel.json --json
59
+ ```
60
+
61
+ Use **JSON for agents** and stdin for larger generated code:
62
+
63
+ ```bash
64
+ cat <<'PY' | jk exec -f /path/to/kernel.json --json --stdin
65
+ import pandas as pd
66
+
67
+ summary = {
68
+ "variables": sorted(name for name in globals() if not name.startswith("_")),
69
+ "answer": 6 * 7,
70
+ }
71
+ summary
72
+ PY
73
+ ```
74
+
75
+ Other supported forms:
76
+
77
+ ```bash
78
+ jk exec -f /path/to/kernel.json --file script.py
79
+ jk -f /path/to/kernel.json --json eval "x + 1"
80
+ export JK_CONNECTION_FILE=/path/to/kernel.json
81
+ jk eval "df.shape"
82
+ ```
83
+
84
+ The JSON response includes status, stdout, stderr, rich display outputs, final `text/plain` result, parsed Python literal when possible, traceback details, elapsed time, timeout state, and message id.
85
+
86
+ ## Using `jk` from Codex
87
+
88
+ One working pattern is to start Codex with workspace sandboxing and on-request approvals:
89
+
90
+ ```bash
91
+ codex -s workspace-write -a on-request
92
+ ```
93
+
94
+ The exact command and permission flow may change across Codex versions, approval policies, sandbox settings, and local configuration. The important point is that Codex may need permission to run `jk` outside its command sandbox so it can open the local Jupyter kernel connection.
95
+
96
+ Give Codex the **active kernel connection information**. You can copy it from
97
+ `%connect_info`, or use
98
+ [`spyder-copy-current`](https://github.com/hruskamiro/spyder-copy-current) and
99
+ press `Ctrl+Alt+K` in Spyder to copy the current console's connection
100
+ information.
101
+
102
+ Ask Codex explicitly to **run `jk` outside its command sandbox**. For example:
103
+
104
+ ```text
105
+ Connect to this exact Jupyter kernel using jk. Run jk with an elevated request
106
+ and ask for reusable approval of the jk executable prefix.
107
+
108
+ <paste the kernel connection information here>
109
+ ```
110
+
111
+ Approve the permission request shown by Codex. If the interface offers reusable command-prefix approval, scope it to the resolved `jk` executable, which can be found with `command -v jk`.
112
+
113
+ **Approving `jk` allows arbitrary code execution in the connected Jupyter
114
+ kernel.** Treat this as execution access to that live Python session, even when
115
+ the rest of the Codex session remains workspace-sandboxed.
116
+
117
+ ## JSON Contract
118
+
119
+ Successful JSON responses include:
120
+
121
+ ```json
122
+ {
123
+ "status": "ok",
124
+ "ok": true,
125
+ "execution_count": 12,
126
+ "stdout": "",
127
+ "stderr": "",
128
+ "outputs": [],
129
+ "result_text": "42",
130
+ "result_python": 42,
131
+ "ename": null,
132
+ "evalue": null,
133
+ "traceback": [],
134
+ "elapsed_seconds": 0.01,
135
+ "timed_out": false,
136
+ "msg_id": "..."
137
+ }
138
+ ```
139
+
140
+ Exit codes:
141
+
142
+ ```text
143
+ 0 kernel execution succeeded
144
+ 1 kernel execution raised an error
145
+ 2 client or argument error
146
+ 124 client timed out waiting for the kernel
147
+ ```
148
+
149
+ Timeouts only stop the client wait. With only a connection file, `jk` cannot reliably kill or interrupt an arbitrary kernel process, so timed-out execution may continue in the kernel.
150
+
151
+ ## Python API
152
+
153
+ ```python
154
+ from jupyter_kernel_client import execute, eval_expression, get_variable
155
+
156
+ response = eval_expression("/path/to/kernel.json", "x + 1")
157
+ if response.ok:
158
+ print(response.result_python)
159
+ ```
160
+
161
+ ## License
162
+
163
+ MIT.
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,58 @@
1
+ [metadata]
2
+ name = jupyter-kernel-cli
3
+ version = 0.1.0
4
+ description = Agent-friendly CLI and Python API for talking to existing Jupyter kernels.
5
+ long_description = file: README.md
6
+ long_description_content_type = text/markdown
7
+ author = Miroslav Hruska
8
+ author_email = hruska.miro@gmail.com
9
+ license = MIT
10
+ license_files = LICENSE
11
+ url = https://github.com/hruskamiro/jupyter-kernel-client
12
+ project_urls =
13
+ Homepage = https://github.com/hruskamiro/jupyter-kernel-client
14
+ Repository = https://github.com/hruskamiro/jupyter-kernel-client
15
+ Issues = https://github.com/hruskamiro/jupyter-kernel-client/issues
16
+ keywords = jupyter, kernel, cli, automation, agent
17
+ classifiers =
18
+ Development Status :: 3 - Alpha
19
+ Environment :: Console
20
+ Intended Audience :: Developers
21
+ Operating System :: OS Independent
22
+ Programming Language :: Python
23
+ Programming Language :: Python :: 3
24
+ Programming Language :: Python :: 3.8
25
+ Programming Language :: Python :: 3.9
26
+ Programming Language :: Python :: 3.10
27
+ Programming Language :: Python :: 3.11
28
+ Programming Language :: Python :: 3.12
29
+ Programming Language :: Python :: 3 :: Only
30
+ Topic :: Software Development
31
+ Topic :: Utilities
32
+
33
+ [options]
34
+ package_dir =
35
+ = src
36
+ packages = find:
37
+ python_requires = >=3.8
38
+ install_requires =
39
+ jupyter-client>=7.0
40
+ include_package_data = True
41
+
42
+ [options.packages.find]
43
+ where = src
44
+ include = jupyter_kernel_client*
45
+
46
+ [options.extras_require]
47
+ dev =
48
+ build>=1.0
49
+ twine>=5.0
50
+
51
+ [options.entry_points]
52
+ console_scripts =
53
+ jk = jupyter_kernel_client.cli:main
54
+
55
+ [egg_info]
56
+ tag_build =
57
+ tag_date = 0
58
+
@@ -0,0 +1,4 @@
1
+ from setuptools import setup
2
+
3
+
4
+ setup()
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: jupyter-kernel-cli
3
+ Version: 0.1.0
4
+ Summary: Agent-friendly CLI and Python API for talking to existing Jupyter kernels.
5
+ Home-page: https://github.com/hruskamiro/jupyter-kernel-client
6
+ Author: Miroslav Hruska
7
+ Author-email: hruska.miro@gmail.com
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/hruskamiro/jupyter-kernel-client
10
+ Project-URL: Repository, https://github.com/hruskamiro/jupyter-kernel-client
11
+ Project-URL: Issues, https://github.com/hruskamiro/jupyter-kernel-client/issues
12
+ Keywords: jupyter,kernel,cli,automation,agent
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3 :: Only
25
+ Classifier: Topic :: Software Development
26
+ Classifier: Topic :: Utilities
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: jupyter-client>=7.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: build>=1.0; extra == "dev"
33
+ Requires-Dist: twine>=5.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # jupyter-kernel-client
37
+
38
+ `jk` is a small command-line tool and Python library for **executing code in an existing Jupyter kernel**.
39
+
40
+ It is designed for scripts and AI agents that need a **stable, machine-readable way to inspect or modify a live Python session**.
41
+
42
+ ## Intended Use Case
43
+
44
+ You are working in an IPython console, Spyder console, notebook kernel, or other Jupyter-backed Python session. **The session already has important state loaded:** imports, data frames, models, helper functions, configuration, intermediate results, and whatever else you have built up interactively.
45
+
46
+ **Instead of asking an AI agent to recreate that state from scratch, give it access to the existing kernel.** Export or copy the kernel connection information, tell the agent to use `jk`, and let it inspect variables, run experiments, evaluate expressions, and return structured output from the same live Python process you are using.
47
+
48
+ The workflow is:
49
+
50
+ 1. **Work normally** in an IPython, Spyder, notebook, or other Jupyter-backed console.
51
+ 2. **Load the state you care about:** data, objects, functions, imports, models, and intermediate results.
52
+ 3. **Copy the active kernel connection info**, for example with `%connect_info`.
53
+ 4. **Give that connection info to Codex or another agent.**
54
+ 5. **Tell the agent to use `jk`** to connect to that exact kernel.
55
+ 6. **Let the agent inspect and experiment** with `jk exec`, `jk eval`, `jk get`, and `jk vars`.
56
+
57
+ This is useful when the hard part is **not writing code from a blank environment**, but **exploring and manipulating the state that already exists in a live session**.
58
+
59
+ ## Install
60
+
61
+ Recommended:
62
+
63
+ ```bash
64
+ pipx install jupyter-kernel-cli
65
+ ```
66
+
67
+ Other install paths:
68
+
69
+ ```bash
70
+ pipx install git+https://github.com/hruskamiro/jupyter-kernel-client.git
71
+ pipx upgrade jupyter-kernel-cli
72
+ pipx install --force .
73
+ python -m pip install -e ".[dev]"
74
+ ```
75
+
76
+ Check:
77
+
78
+ ```bash
79
+ jk --help
80
+ ```
81
+
82
+ ## Usage
83
+
84
+ Common commands:
85
+
86
+ ```bash
87
+ jk kernels
88
+ jk kernels --probe
89
+ jk exec -f /path/to/kernel.json "x = 41"
90
+ jk eval -f /path/to/kernel.json "x + 1"
91
+ jk get -f /path/to/kernel.json x
92
+ jk vars -f /path/to/kernel.json --json
93
+ jk demo -f /path/to/kernel.json --json
94
+ ```
95
+
96
+ Use **JSON for agents** and stdin for larger generated code:
97
+
98
+ ```bash
99
+ cat <<'PY' | jk exec -f /path/to/kernel.json --json --stdin
100
+ import pandas as pd
101
+
102
+ summary = {
103
+ "variables": sorted(name for name in globals() if not name.startswith("_")),
104
+ "answer": 6 * 7,
105
+ }
106
+ summary
107
+ PY
108
+ ```
109
+
110
+ Other supported forms:
111
+
112
+ ```bash
113
+ jk exec -f /path/to/kernel.json --file script.py
114
+ jk -f /path/to/kernel.json --json eval "x + 1"
115
+ export JK_CONNECTION_FILE=/path/to/kernel.json
116
+ jk eval "df.shape"
117
+ ```
118
+
119
+ The JSON response includes status, stdout, stderr, rich display outputs, final `text/plain` result, parsed Python literal when possible, traceback details, elapsed time, timeout state, and message id.
120
+
121
+ ## Using `jk` from Codex
122
+
123
+ One working pattern is to start Codex with workspace sandboxing and on-request approvals:
124
+
125
+ ```bash
126
+ codex -s workspace-write -a on-request
127
+ ```
128
+
129
+ The exact command and permission flow may change across Codex versions, approval policies, sandbox settings, and local configuration. The important point is that Codex may need permission to run `jk` outside its command sandbox so it can open the local Jupyter kernel connection.
130
+
131
+ Give Codex the **active kernel connection information**. You can copy it from
132
+ `%connect_info`, or use
133
+ [`spyder-copy-current`](https://github.com/hruskamiro/spyder-copy-current) and
134
+ press `Ctrl+Alt+K` in Spyder to copy the current console's connection
135
+ information.
136
+
137
+ Ask Codex explicitly to **run `jk` outside its command sandbox**. For example:
138
+
139
+ ```text
140
+ Connect to this exact Jupyter kernel using jk. Run jk with an elevated request
141
+ and ask for reusable approval of the jk executable prefix.
142
+
143
+ <paste the kernel connection information here>
144
+ ```
145
+
146
+ Approve the permission request shown by Codex. If the interface offers reusable command-prefix approval, scope it to the resolved `jk` executable, which can be found with `command -v jk`.
147
+
148
+ **Approving `jk` allows arbitrary code execution in the connected Jupyter
149
+ kernel.** Treat this as execution access to that live Python session, even when
150
+ the rest of the Codex session remains workspace-sandboxed.
151
+
152
+ ## JSON Contract
153
+
154
+ Successful JSON responses include:
155
+
156
+ ```json
157
+ {
158
+ "status": "ok",
159
+ "ok": true,
160
+ "execution_count": 12,
161
+ "stdout": "",
162
+ "stderr": "",
163
+ "outputs": [],
164
+ "result_text": "42",
165
+ "result_python": 42,
166
+ "ename": null,
167
+ "evalue": null,
168
+ "traceback": [],
169
+ "elapsed_seconds": 0.01,
170
+ "timed_out": false,
171
+ "msg_id": "..."
172
+ }
173
+ ```
174
+
175
+ Exit codes:
176
+
177
+ ```text
178
+ 0 kernel execution succeeded
179
+ 1 kernel execution raised an error
180
+ 2 client or argument error
181
+ 124 client timed out waiting for the kernel
182
+ ```
183
+
184
+ Timeouts only stop the client wait. With only a connection file, `jk` cannot reliably kill or interrupt an arbitrary kernel process, so timed-out execution may continue in the kernel.
185
+
186
+ ## Python API
187
+
188
+ ```python
189
+ from jupyter_kernel_client import execute, eval_expression, get_variable
190
+
191
+ response = eval_expression("/path/to/kernel.json", "x + 1")
192
+ if response.ok:
193
+ print(response.result_python)
194
+ ```
195
+
196
+ ## License
197
+
198
+ MIT.
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.cfg
5
+ setup.py
6
+ src/jupyter_kernel_cli.egg-info/PKG-INFO
7
+ src/jupyter_kernel_cli.egg-info/SOURCES.txt
8
+ src/jupyter_kernel_cli.egg-info/dependency_links.txt
9
+ src/jupyter_kernel_cli.egg-info/entry_points.txt
10
+ src/jupyter_kernel_cli.egg-info/requires.txt
11
+ src/jupyter_kernel_cli.egg-info/top_level.txt
12
+ src/jupyter_kernel_client/__init__.py
13
+ src/jupyter_kernel_client/cli.py
14
+ src/jupyter_kernel_client/client.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ jk = jupyter_kernel_client.cli:main
@@ -0,0 +1,5 @@
1
+ jupyter-client>=7.0
2
+
3
+ [dev]
4
+ build>=1.0
5
+ twine>=5.0
@@ -0,0 +1 @@
1
+ jupyter_kernel_client
@@ -0,0 +1,21 @@
1
+ """Small client for executing code in an existing Jupyter kernel."""
2
+
3
+ from .client import (
4
+ KernelClientError,
5
+ KernelResponse,
6
+ RichOutput,
7
+ execute,
8
+ eval_expression,
9
+ get_variable,
10
+ kernel_is_ready,
11
+ )
12
+
13
+ __all__ = [
14
+ "KernelClientError",
15
+ "KernelResponse",
16
+ "RichOutput",
17
+ "execute",
18
+ "eval_expression",
19
+ "get_variable",
20
+ "kernel_is_ready",
21
+ ]
@@ -0,0 +1,276 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Iterable, List, Optional
9
+
10
+ from jupyter_core.paths import jupyter_runtime_dir
11
+
12
+ from .client import KernelClientError, KernelResponse, execute, eval_expression, get_variable, kernel_is_ready
13
+
14
+
15
+ def _default_connection_file() -> Optional[str]:
16
+ return os.environ.get("JK_CONNECTION_FILE") or os.environ.get("JUPYTER_CONNECTION_FILE")
17
+
18
+
19
+ def _runtime_connection_files(limit: Optional[int] = None) -> List[Path]:
20
+ runtime = Path(jupyter_runtime_dir()).expanduser()
21
+ if not runtime.exists():
22
+ return []
23
+ files = sorted(runtime.glob("kernel-*.json"), key=lambda path: path.stat().st_mtime, reverse=True)
24
+ if limit is not None:
25
+ return files[:limit]
26
+ return files
27
+
28
+
29
+ def _kernel_record(path: Path, *, probe: bool, timeout: float) -> dict:
30
+ record = {
31
+ "path": str(path),
32
+ "modified": path.stat().st_mtime,
33
+ "alive": None,
34
+ "error": None,
35
+ }
36
+ if not probe:
37
+ return record
38
+
39
+ try:
40
+ kernel_is_ready(str(path), timeout=timeout)
41
+ record["alive"] = True
42
+ except Exception as exc:
43
+ record["alive"] = False
44
+ record["error"] = str(exc)
45
+ return record
46
+
47
+
48
+ def _read_code(args: argparse.Namespace) -> str:
49
+ sources = [bool(args.code), bool(args.file), bool(args.stdin)]
50
+ if sum(sources) != 1:
51
+ raise KernelClientError("Provide exactly one code source: argument, --file, or --stdin.")
52
+ if args.stdin:
53
+ return sys.stdin.read()
54
+ if args.file:
55
+ return Path(args.file).expanduser().read_text()
56
+ return args.code
57
+
58
+
59
+ def _print_human(response: KernelResponse) -> int:
60
+ if response.stdout:
61
+ sys.stdout.write(response.stdout)
62
+ if response.stderr:
63
+ sys.stderr.write(response.stderr)
64
+
65
+ if response.status == "timeout":
66
+ sys.stderr.write(f"Timed out after {response.elapsed_seconds:.2f}s. Kernel execution may still be running.\n")
67
+ return 124
68
+
69
+ if response.status == "error":
70
+ if response.traceback:
71
+ sys.stderr.write("\n".join(response.traceback) + "\n")
72
+ elif response.ename or response.evalue:
73
+ sys.stderr.write(f"{response.ename}: {response.evalue}\n")
74
+ return 1
75
+
76
+ if response.result_text is not None:
77
+ sys.stdout.write(f"{response.result_text}\n")
78
+ return 0
79
+
80
+
81
+ def _emit_response(response: KernelResponse, *, json_output: bool) -> int:
82
+ if json_output:
83
+ print(json.dumps(response.to_dict(), ensure_ascii=True))
84
+ if response.status == "timeout":
85
+ return 124
86
+ return 0 if response.ok else 1
87
+ return _print_human(response)
88
+
89
+
90
+ def _resolve_connection_file(args: argparse.Namespace) -> str:
91
+ connection_file = args.connection_file or _default_connection_file()
92
+ if connection_file:
93
+ return str(Path(connection_file).expanduser())
94
+ raise KernelClientError(
95
+ "No connection file provided. Use --connection-file, JK_CONNECTION_FILE, "
96
+ "JUPYTER_CONNECTION_FILE, or run `jk kernels` to list candidates."
97
+ )
98
+
99
+
100
+ def _add_common_options(parser: argparse.ArgumentParser) -> None:
101
+ parser.add_argument(
102
+ "--connection-file",
103
+ "-f",
104
+ help="Path to a Jupyter kernel connection JSON file. Defaults to JK_CONNECTION_FILE or JUPYTER_CONNECTION_FILE.",
105
+ )
106
+ parser.add_argument("--timeout", type=float, default=30.0, help="Seconds to wait for execution completion.")
107
+ parser.add_argument("--json", action="store_true", help="Emit stable structured JSON output.")
108
+
109
+
110
+ def _vars_code(*, include_private: bool, repr_length: int) -> str:
111
+ private_check = "True" if include_private else "not name.startswith('_')"
112
+ return f"""
113
+ [
114
+ {{
115
+ "name": name,
116
+ "type": type(value).__module__ + "." + type(value).__qualname__,
117
+ "repr": repr(value)[:{repr_length}],
118
+ }}
119
+ for name, value in sorted(globals().items())
120
+ if {private_check}
121
+ and name not in {{"In", "Out", "exit", "quit", "get_ipython"}}
122
+ ]
123
+ """.strip()
124
+
125
+
126
+ def _run_demo(connection_file: str, *, timeout: float, json_output: bool) -> int:
127
+ snippets = [
128
+ ("stdout_and_result", "print('hello from jk')\n21 * 2"),
129
+ ("state_write", "jk_demo_value = {'ready': True, 'items': [1, 2, 3]}\njk_demo_value"),
130
+ ("state_read", "jk_demo_value"),
131
+ ("rich_output", "from IPython.display import display, HTML\ndisplay(HTML('<b>jk html output</b>'))\n'ok'"),
132
+ ]
133
+
134
+ results = []
135
+ exit_code = 0
136
+ for name, code in snippets:
137
+ response = execute(connection_file, code, timeout=timeout)
138
+ results.append({"name": name, "response": response.to_dict()})
139
+ if not response.ok and exit_code == 0:
140
+ exit_code = 124 if response.status == "timeout" else 1
141
+
142
+ if json_output:
143
+ print(json.dumps({"status": "ok" if exit_code == 0 else "error", "ok": exit_code == 0, "checks": results}, ensure_ascii=True))
144
+ return exit_code
145
+
146
+ for result in results:
147
+ response = result["response"]
148
+ status = "ok" if response["ok"] else response["status"]
149
+ print(f"{result['name']}: {status}")
150
+ if response["stdout"]:
151
+ print(response["stdout"], end="")
152
+ if response["result_text"] is not None:
153
+ print(response["result_text"])
154
+ if response["outputs"]:
155
+ print(f"outputs: {len(response['outputs'])}")
156
+ return exit_code
157
+
158
+
159
+ def _build_parser() -> argparse.ArgumentParser:
160
+ parser = argparse.ArgumentParser(prog="jk", description="Execute code in an existing Jupyter kernel.")
161
+ subparsers = parser.add_subparsers(dest="command", required=True)
162
+
163
+ exec_parser = subparsers.add_parser("exec", help="Execute code.")
164
+ _add_common_options(exec_parser)
165
+ exec_parser.add_argument("code", nargs="?", help="Code to execute.")
166
+ exec_parser.add_argument("--file", "-i", help="Read code from a file.")
167
+ exec_parser.add_argument("--stdin", action="store_true", help="Read code from standard input.")
168
+ exec_parser.add_argument("--silent", action="store_true", help="Execute silently.")
169
+ exec_parser.add_argument("--no-history", action="store_true", help="Do not store execution in kernel history.")
170
+
171
+ eval_parser = subparsers.add_parser("eval", help="Evaluate a Python expression.")
172
+ _add_common_options(eval_parser)
173
+ eval_parser.add_argument("expression", help="Python expression to evaluate.")
174
+
175
+ get_parser = subparsers.add_parser("get", help="Get a variable by name.")
176
+ _add_common_options(get_parser)
177
+ get_parser.add_argument("name", help="Variable name to fetch from the kernel.")
178
+
179
+ vars_parser = subparsers.add_parser("vars", help="Inspect user-visible variables in the kernel namespace.")
180
+ _add_common_options(vars_parser)
181
+ vars_parser.add_argument("--include-private", action="store_true", help="Include names beginning with underscore.")
182
+ vars_parser.add_argument("--repr-length", type=int, default=160, help="Maximum repr length per variable.")
183
+
184
+ demo_parser = subparsers.add_parser("demo", help="Run a short capability demo against a kernel.")
185
+ _add_common_options(demo_parser)
186
+
187
+ kernels_parser = subparsers.add_parser("kernels", help="List recent connection files in the Jupyter runtime directory.")
188
+ kernels_parser.add_argument("--json", action="store_true", help="Emit JSON.")
189
+ kernels_parser.add_argument("--limit", type=int, default=20, help="Maximum files to list. Use 0 for no limit.")
190
+ kernels_parser.add_argument("--probe", action="store_true", help="Connect to each listed file and report whether it responds.")
191
+ kernels_parser.add_argument("--timeout", type=float, default=1.0, help="Seconds to wait per probed kernel.")
192
+
193
+ return parser
194
+
195
+
196
+ def _normalize_argv(argv: Optional[Iterable[str]]) -> Optional[List[str]]:
197
+ if argv is None:
198
+ values = sys.argv[1:]
199
+ else:
200
+ values = list(argv)
201
+
202
+ commands = {"exec", "eval", "get", "vars", "demo", "kernels"}
203
+ command_positions = [index for index, value in enumerate(values) if value in commands]
204
+ if not command_positions:
205
+ return values if argv is not None else None
206
+
207
+ command_index = command_positions[0]
208
+ if command_index == 0:
209
+ return values if argv is not None else None
210
+
211
+ normalized = [values[command_index], *values[:command_index], *values[command_index + 1 :]]
212
+ return normalized
213
+
214
+
215
+ def _list_kernels(*, json_output: bool, limit: int, probe: bool, timeout: float) -> int:
216
+ files = _runtime_connection_files(limit=None if limit == 0 else limit)
217
+ records = [_kernel_record(path, probe=probe, timeout=timeout) for path in files]
218
+ if json_output:
219
+ print(json.dumps(records, ensure_ascii=True))
220
+ return 0
221
+ if not records:
222
+ print("No kernel connection files found.")
223
+ return 1
224
+ for record in records:
225
+ if probe:
226
+ status = "alive" if record["alive"] else "dead"
227
+ print(f"{status:5} {record['path']}")
228
+ else:
229
+ print(record["path"])
230
+ return 0
231
+
232
+
233
+ def main(argv: Optional[Iterable[str]] = None) -> int:
234
+ parser = _build_parser()
235
+ args = parser.parse_args(_normalize_argv(argv))
236
+
237
+ try:
238
+ if args.command == "kernels":
239
+ return _list_kernels(json_output=args.json, limit=args.limit, probe=args.probe, timeout=args.timeout)
240
+
241
+ connection_file = _resolve_connection_file(args)
242
+ if args.command == "exec":
243
+ code = _read_code(args)
244
+ response = execute(
245
+ connection_file,
246
+ code,
247
+ timeout=args.timeout,
248
+ silent=args.silent,
249
+ store_history=not args.no_history,
250
+ )
251
+ elif args.command == "eval":
252
+ response = eval_expression(connection_file, args.expression, timeout=args.timeout)
253
+ elif args.command == "get":
254
+ response = get_variable(connection_file, args.name, timeout=args.timeout)
255
+ elif args.command == "vars":
256
+ response = execute(
257
+ connection_file,
258
+ _vars_code(include_private=args.include_private, repr_length=args.repr_length),
259
+ timeout=args.timeout,
260
+ )
261
+ elif args.command == "demo":
262
+ return _run_demo(connection_file, timeout=args.timeout, json_output=args.json)
263
+ else:
264
+ parser.error(f"Unknown command: {args.command}")
265
+ except Exception as exc:
266
+ if getattr(args, "json", False):
267
+ print(json.dumps({"status": "client_error", "ok": False, "error": str(exc)}, ensure_ascii=True))
268
+ else:
269
+ print(f"Client error: {exc}", file=sys.stderr)
270
+ return 2
271
+
272
+ return _emit_response(response, json_output=args.json)
273
+
274
+
275
+ if __name__ == "__main__":
276
+ raise SystemExit(main())
@@ -0,0 +1,239 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import json
5
+ import time
6
+ import uuid
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from queue import Empty
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from jupyter_client import BlockingKernelClient
13
+
14
+
15
+ class KernelClientError(RuntimeError):
16
+ """Raised when the client cannot complete a request."""
17
+
18
+
19
+ @dataclass
20
+ class RichOutput:
21
+ output_type: str
22
+ data: Dict[str, Any] = field(default_factory=dict)
23
+ metadata: Dict[str, Any] = field(default_factory=dict)
24
+ transient: Dict[str, Any] = field(default_factory=dict)
25
+ execution_count: Optional[int] = None
26
+ text: Optional[str] = None
27
+
28
+ def to_dict(self) -> Dict[str, Any]:
29
+ return {
30
+ "output_type": self.output_type,
31
+ "data": self.data,
32
+ "metadata": self.metadata,
33
+ "transient": self.transient,
34
+ "execution_count": self.execution_count,
35
+ "text": self.text,
36
+ }
37
+
38
+
39
+ @dataclass
40
+ class KernelResponse:
41
+ status: str
42
+ execution_count: Optional[int]
43
+ stdout: str
44
+ stderr: str
45
+ outputs: List[RichOutput]
46
+ result_text: Optional[str]
47
+ result_python: Any
48
+ ename: Optional[str]
49
+ evalue: Optional[str]
50
+ traceback: List[str]
51
+ elapsed_seconds: float
52
+ timed_out: bool
53
+ msg_id: str
54
+
55
+ @property
56
+ def ok(self) -> bool:
57
+ return self.status == "ok" and not self.timed_out
58
+
59
+ def to_dict(self) -> Dict[str, Any]:
60
+ return {
61
+ "status": self.status,
62
+ "ok": self.ok,
63
+ "execution_count": self.execution_count,
64
+ "stdout": self.stdout,
65
+ "stderr": self.stderr,
66
+ "outputs": [output.to_dict() for output in self.outputs],
67
+ "result_text": self.result_text,
68
+ "result_python": self.result_python,
69
+ "ename": self.ename,
70
+ "evalue": self.evalue,
71
+ "traceback": self.traceback,
72
+ "elapsed_seconds": self.elapsed_seconds,
73
+ "timed_out": self.timed_out,
74
+ "msg_id": self.msg_id,
75
+ }
76
+
77
+
78
+ def _parse_python_literal(text: Optional[str]) -> Any:
79
+ if text is None:
80
+ return None
81
+ try:
82
+ return ast.literal_eval(text)
83
+ except Exception:
84
+ return text
85
+
86
+
87
+ def _make_client(connection_file: str, *, ready_timeout: float = 5.0) -> BlockingKernelClient:
88
+ path = Path(connection_file).expanduser()
89
+ if not path.exists():
90
+ raise KernelClientError(f"Connection file does not exist: {path}")
91
+
92
+ client = BlockingKernelClient(connection_file=str(path))
93
+ client.load_connection_file()
94
+ client.start_channels()
95
+ try:
96
+ client.wait_for_ready(timeout=ready_timeout)
97
+ except Exception as exc:
98
+ client.stop_channels()
99
+ raise KernelClientError(f"Kernel did not become ready within {ready_timeout:.1f}s: {exc}") from exc
100
+ return client
101
+
102
+
103
+ def kernel_is_ready(connection_file: str, *, timeout: float = 1.0) -> None:
104
+ """Raise KernelClientError if the kernel connection file does not respond."""
105
+ client = _make_client(connection_file, ready_timeout=timeout)
106
+ client.stop_channels()
107
+
108
+
109
+ def _json_safe(value: Any) -> Any:
110
+ try:
111
+ json.dumps(value)
112
+ return value
113
+ except TypeError:
114
+ return repr(value)
115
+
116
+
117
+ def _message_output(msg_type: str, content: Dict[str, Any]) -> Optional[RichOutput]:
118
+ if msg_type not in {"execute_result", "display_data", "update_display_data"}:
119
+ return None
120
+
121
+ data = {key: _json_safe(value) for key, value in content.get("data", {}).items()}
122
+ text = data.get("text/plain")
123
+ return RichOutput(
124
+ output_type=msg_type,
125
+ data=data,
126
+ metadata=content.get("metadata", {}),
127
+ transient=content.get("transient", {}),
128
+ execution_count=content.get("execution_count"),
129
+ text=text if isinstance(text, str) else None,
130
+ )
131
+
132
+
133
+ def execute(
134
+ connection_file: str,
135
+ code: str,
136
+ *,
137
+ timeout: float = 30.0,
138
+ ready_timeout: float = 5.0,
139
+ silent: bool = False,
140
+ store_history: bool = True,
141
+ allow_stdin: bool = False,
142
+ ) -> KernelResponse:
143
+ """Execute code in an existing Jupyter kernel and collect its IOPub output."""
144
+ if timeout <= 0:
145
+ raise ValueError("timeout must be greater than zero")
146
+
147
+ client = _make_client(connection_file, ready_timeout=ready_timeout)
148
+ stdout_chunks: List[str] = []
149
+ stderr_chunks: List[str] = []
150
+ outputs: List[RichOutput] = []
151
+ result_text: Optional[str] = None
152
+ execution_count: Optional[int] = None
153
+ status = "unknown"
154
+ ename: Optional[str] = None
155
+ evalue: Optional[str] = None
156
+ traceback: List[str] = []
157
+ msg_id = ""
158
+ started = time.monotonic()
159
+ timed_out = False
160
+
161
+ try:
162
+ msg_id = client.execute(
163
+ code,
164
+ silent=silent,
165
+ store_history=store_history,
166
+ user_expressions={},
167
+ allow_stdin=allow_stdin,
168
+ stop_on_error=True,
169
+ )
170
+ deadline = started + timeout
171
+
172
+ while time.monotonic() < deadline:
173
+ remaining = max(0.05, deadline - time.monotonic())
174
+ try:
175
+ msg = client.get_iopub_msg(timeout=remaining)
176
+ except Empty:
177
+ continue
178
+
179
+ if msg.get("parent_header", {}).get("msg_id") != msg_id:
180
+ continue
181
+
182
+ msg_type = msg.get("msg_type")
183
+ content = msg.get("content", {})
184
+
185
+ if msg_type == "stream":
186
+ if content.get("name") == "stderr":
187
+ stderr_chunks.append(content.get("text", ""))
188
+ else:
189
+ stdout_chunks.append(content.get("text", ""))
190
+ elif msg_type in {"execute_result", "display_data", "update_display_data"}:
191
+ output = _message_output(msg_type, content)
192
+ if output is not None:
193
+ outputs.append(output)
194
+ if output.text is not None:
195
+ result_text = output.text
196
+ execution_count = output.execution_count or execution_count
197
+ elif msg_type == "error":
198
+ status = "error"
199
+ ename = content.get("ename")
200
+ evalue = content.get("evalue")
201
+ traceback = content.get("traceback", [])
202
+ elif msg_type == "execute_input":
203
+ execution_count = content.get("execution_count", execution_count)
204
+ elif msg_type == "status" and content.get("execution_state") == "idle":
205
+ if status != "error":
206
+ status = "ok"
207
+ break
208
+
209
+ timed_out = status == "unknown"
210
+ if timed_out:
211
+ status = "timeout"
212
+ finally:
213
+ client.stop_channels()
214
+
215
+ return KernelResponse(
216
+ status=status,
217
+ execution_count=execution_count,
218
+ stdout="".join(stdout_chunks),
219
+ stderr="".join(stderr_chunks),
220
+ outputs=outputs,
221
+ result_text=result_text,
222
+ result_python=_parse_python_literal(result_text),
223
+ ename=ename,
224
+ evalue=evalue,
225
+ traceback=traceback,
226
+ elapsed_seconds=round(time.monotonic() - started, 6),
227
+ timed_out=timed_out,
228
+ msg_id=msg_id,
229
+ )
230
+
231
+
232
+ def eval_expression(connection_file: str, expression: str, *, timeout: float = 30.0) -> KernelResponse:
233
+ sentinel = f"__jk_value_{uuid.uuid4().hex}__"
234
+ code = f"globals()[{sentinel!r}] = ({expression})\nglobals().pop({sentinel!r})"
235
+ return execute(connection_file, code, timeout=timeout)
236
+
237
+
238
+ def get_variable(connection_file: str, variable_name: str, *, timeout: float = 30.0) -> KernelResponse:
239
+ return eval_expression(connection_file, variable_name, timeout=timeout)