jupyter-jcli 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.
Files changed (33) hide show
  1. jupyter_jcli-0.1.0/.github/workflows/release.yml +33 -0
  2. jupyter_jcli-0.1.0/.github/workflows/test.yml +27 -0
  3. jupyter_jcli-0.1.0/.gitignore +3 -0
  4. jupyter_jcli-0.1.0/LICENSE +21 -0
  5. jupyter_jcli-0.1.0/PKG-INFO +155 -0
  6. jupyter_jcli-0.1.0/README.md +133 -0
  7. jupyter_jcli-0.1.0/jupyter_jcli/__init__.py +1 -0
  8. jupyter_jcli-0.1.0/jupyter_jcli/__main__.py +3 -0
  9. jupyter_jcli-0.1.0/jupyter_jcli/cli.py +72 -0
  10. jupyter_jcli-0.1.0/jupyter_jcli/commands/__init__.py +0 -0
  11. jupyter_jcli-0.1.0/jupyter_jcli/commands/exec_cmd.py +128 -0
  12. jupyter_jcli-0.1.0/jupyter_jcli/commands/healthcheck.py +27 -0
  13. jupyter_jcli-0.1.0/jupyter_jcli/commands/kernel_cmd.py +47 -0
  14. jupyter_jcli-0.1.0/jupyter_jcli/commands/kernelspec.py +33 -0
  15. jupyter_jcli-0.1.0/jupyter_jcli/commands/session.py +76 -0
  16. jupyter_jcli-0.1.0/jupyter_jcli/config.py +17 -0
  17. jupyter_jcli-0.1.0/jupyter_jcli/executor.py +96 -0
  18. jupyter_jcli-0.1.0/jupyter_jcli/kernel.py +36 -0
  19. jupyter_jcli-0.1.0/jupyter_jcli/notebook_writer.py +92 -0
  20. jupyter_jcli-0.1.0/jupyter_jcli/output.py +31 -0
  21. jupyter_jcli-0.1.0/jupyter_jcli/parser.py +154 -0
  22. jupyter_jcli-0.1.0/jupyter_jcli/server.py +99 -0
  23. jupyter_jcli-0.1.0/pyproject.toml +40 -0
  24. jupyter_jcli-0.1.0/skills/j-cli/SKILL.md +259 -0
  25. jupyter_jcli-0.1.0/tests/__init__.py +0 -0
  26. jupyter_jcli-0.1.0/tests/conftest.py +94 -0
  27. jupyter_jcli-0.1.0/tests/test_exec.py +194 -0
  28. jupyter_jcli-0.1.0/tests/test_healthcheck.py +33 -0
  29. jupyter_jcli-0.1.0/tests/test_kernel_cmd.py +47 -0
  30. jupyter_jcli-0.1.0/tests/test_kernelspec.py +31 -0
  31. jupyter_jcli-0.1.0/tests/test_notebook_writeback.py +219 -0
  32. jupyter_jcli-0.1.0/tests/test_session.py +66 -0
  33. jupyter_jcli-0.1.0/uv.lock +2690 -0
@@ -0,0 +1,33 @@
1
+ name: Release & Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v6
19
+
20
+ - name: Set up Python
21
+ run: uv python install 3.12
22
+
23
+ - name: Build package
24
+ run: uv build
25
+
26
+ - name: Publish to PyPI
27
+ run: uv publish -t ${{ secrets.PYPI_TOKEN }}
28
+
29
+ - name: Create GitHub Release
30
+ uses: softprops/action-gh-release@v2
31
+ with:
32
+ generate_release_notes: true
33
+ files: dist/*
@@ -0,0 +1,27 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ matrix:
12
+ python-version: ["3.10", "3.12"]
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v6
19
+
20
+ - name: Set up Python ${{ matrix.python-version }}
21
+ run: uv python install ${{ matrix.python-version }}
22
+
23
+ - name: Install dependencies
24
+ run: uv sync --extra test
25
+
26
+ - name: Run tests
27
+ run: uv run pytest -v
@@ -0,0 +1,3 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tttpob
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,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: jupyter-jcli
3
+ Version: 0.1.0
4
+ Summary: CLI tool for LLM agents to operate Jupyter Lab servers
5
+ Author-email: tttpob <i@tpob.io>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: click>=8
12
+ Requires-Dist: jupyter-kernel-client>=0.7.3
13
+ Requires-Dist: jupyter-server-client
14
+ Requires-Dist: nbformat>=5
15
+ Provides-Extra: test
16
+ Requires-Dist: ipykernel; extra == 'test'
17
+ Requires-Dist: jupyter-server<3,>=2; extra == 'test'
18
+ Requires-Dist: matplotlib; extra == 'test'
19
+ Requires-Dist: pytest-asyncio; extra == 'test'
20
+ Requires-Dist: pytest>=8; extra == 'test'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # jupyter-jcli
24
+
25
+ CLI tool for LLM agents to operate Jupyter Lab servers.
26
+
27
+ j-cli enables AI agents (and humans) to remotely control Jupyter servers — execute code in kernels, manage sessions, and write outputs back to notebooks, all from the command line.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ # from source
33
+ uv sync
34
+ ```
35
+
36
+ Requires Python 3.10+.
37
+
38
+ ## Quick Start
39
+
40
+ ```bash
41
+ # set connection (or pass via -s / -t flags)
42
+ export JCLI_JUPYTER_SERVER_URL=http://localhost:8888
43
+ export JCLI_JUPYTER_SERVER_TOKEN=your-token
44
+
45
+ # check connectivity
46
+ j-cli healthcheck
47
+
48
+ # create a session and execute code
49
+ j-cli session create --kernel python3 --name my-session
50
+ j-cli exec <session_id> --code "print('hello world')"
51
+ ```
52
+
53
+ ## Commands
54
+
55
+ ### Global Options
56
+
57
+ | Flag | Description |
58
+ |------|-------------|
59
+ | `-s`, `--server-url` | Jupyter server URL (env: `JCLI_JUPYTER_SERVER_URL`, default: `http://localhost:8888`) |
60
+ | `-t`, `--token` | Auth token (env: `JCLI_JUPYTER_SERVER_TOKEN`) |
61
+ | `-j`, `--json` | Output as JSON for programmatic use |
62
+ | `--version` | Show version |
63
+
64
+ ### `healthcheck`
65
+
66
+ Check server connectivity and running kernel count.
67
+
68
+ ```bash
69
+ j-cli healthcheck
70
+ ```
71
+
72
+ ### `kernelspec list`
73
+
74
+ List available kernel specifications.
75
+
76
+ ```bash
77
+ j-cli kernelspec list
78
+ ```
79
+
80
+ ### `session`
81
+
82
+ ```bash
83
+ j-cli session create --kernel python3 --name my-session
84
+ j-cli session list
85
+ j-cli session kill <session_id>
86
+ ```
87
+
88
+ ### `kernel`
89
+
90
+ ```bash
91
+ j-cli kernel interrupt <session_id>
92
+ j-cli kernel restart <session_id>
93
+ ```
94
+
95
+ ### `exec`
96
+
97
+ Execute code in a kernel session. Supports inline code, py:percent files, and Jupyter notebooks.
98
+
99
+ ```bash
100
+ # inline code
101
+ j-cli exec <session_id> --code "import pandas as pd; df = pd.read_csv('data.csv'); df.head()"
102
+
103
+ # execute from py:percent file
104
+ j-cli exec <session_id> --file analysis.py
105
+
106
+ # execute specific cells from a notebook
107
+ j-cli exec <session_id> --file notebook.ipynb --cell 0:3
108
+
109
+ # execute a single cell
110
+ j-cli exec <session_id> --file notebook.ipynb --cell 5
111
+ ```
112
+
113
+ **Cell spec formats** (0-indexed):
114
+
115
+ | Spec | Meaning |
116
+ |------|---------|
117
+ | `3` | Cell 3 only |
118
+ | `3:7` | Cells 3, 4, 5, 6 |
119
+ | `3:` | Cell 3 to end |
120
+ | `:5` | Cells 0 through 4 |
121
+
122
+ **Notebook writeback**: When executing from a file, outputs are automatically written back to the paired `.ipynb` file. For `analysis.py`, j-cli looks for `analysis.ipynb` in the same directory.
123
+
124
+ ## Py:Percent Format
125
+
126
+ j-cli supports the [py:percent](https://jupytext.readthedocs.io/en/latest/formats-scripts.html#the-percent-format) format — plain Python files with cell markers:
127
+
128
+ ```python
129
+ # ---
130
+ # jupyter:
131
+ # kernelspec:
132
+ # name: python3
133
+ # ---
134
+
135
+ # %%
136
+ import numpy as np
137
+
138
+ # %%
139
+ x = np.random.randn(100)
140
+ print(x.mean())
141
+ ```
142
+
143
+ ## Development
144
+
145
+ ```bash
146
+ # install with test dependencies
147
+ uv sync --extra test
148
+
149
+ # run tests (requires a real Jupyter server, started automatically by fixtures)
150
+ uv run pytest -v
151
+ ```
152
+
153
+ ## License
154
+
155
+ MIT
@@ -0,0 +1,133 @@
1
+ # jupyter-jcli
2
+
3
+ CLI tool for LLM agents to operate Jupyter Lab servers.
4
+
5
+ j-cli enables AI agents (and humans) to remotely control Jupyter servers — execute code in kernels, manage sessions, and write outputs back to notebooks, all from the command line.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # from source
11
+ uv sync
12
+ ```
13
+
14
+ Requires Python 3.10+.
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ # set connection (or pass via -s / -t flags)
20
+ export JCLI_JUPYTER_SERVER_URL=http://localhost:8888
21
+ export JCLI_JUPYTER_SERVER_TOKEN=your-token
22
+
23
+ # check connectivity
24
+ j-cli healthcheck
25
+
26
+ # create a session and execute code
27
+ j-cli session create --kernel python3 --name my-session
28
+ j-cli exec <session_id> --code "print('hello world')"
29
+ ```
30
+
31
+ ## Commands
32
+
33
+ ### Global Options
34
+
35
+ | Flag | Description |
36
+ |------|-------------|
37
+ | `-s`, `--server-url` | Jupyter server URL (env: `JCLI_JUPYTER_SERVER_URL`, default: `http://localhost:8888`) |
38
+ | `-t`, `--token` | Auth token (env: `JCLI_JUPYTER_SERVER_TOKEN`) |
39
+ | `-j`, `--json` | Output as JSON for programmatic use |
40
+ | `--version` | Show version |
41
+
42
+ ### `healthcheck`
43
+
44
+ Check server connectivity and running kernel count.
45
+
46
+ ```bash
47
+ j-cli healthcheck
48
+ ```
49
+
50
+ ### `kernelspec list`
51
+
52
+ List available kernel specifications.
53
+
54
+ ```bash
55
+ j-cli kernelspec list
56
+ ```
57
+
58
+ ### `session`
59
+
60
+ ```bash
61
+ j-cli session create --kernel python3 --name my-session
62
+ j-cli session list
63
+ j-cli session kill <session_id>
64
+ ```
65
+
66
+ ### `kernel`
67
+
68
+ ```bash
69
+ j-cli kernel interrupt <session_id>
70
+ j-cli kernel restart <session_id>
71
+ ```
72
+
73
+ ### `exec`
74
+
75
+ Execute code in a kernel session. Supports inline code, py:percent files, and Jupyter notebooks.
76
+
77
+ ```bash
78
+ # inline code
79
+ j-cli exec <session_id> --code "import pandas as pd; df = pd.read_csv('data.csv'); df.head()"
80
+
81
+ # execute from py:percent file
82
+ j-cli exec <session_id> --file analysis.py
83
+
84
+ # execute specific cells from a notebook
85
+ j-cli exec <session_id> --file notebook.ipynb --cell 0:3
86
+
87
+ # execute a single cell
88
+ j-cli exec <session_id> --file notebook.ipynb --cell 5
89
+ ```
90
+
91
+ **Cell spec formats** (0-indexed):
92
+
93
+ | Spec | Meaning |
94
+ |------|---------|
95
+ | `3` | Cell 3 only |
96
+ | `3:7` | Cells 3, 4, 5, 6 |
97
+ | `3:` | Cell 3 to end |
98
+ | `:5` | Cells 0 through 4 |
99
+
100
+ **Notebook writeback**: When executing from a file, outputs are automatically written back to the paired `.ipynb` file. For `analysis.py`, j-cli looks for `analysis.ipynb` in the same directory.
101
+
102
+ ## Py:Percent Format
103
+
104
+ j-cli supports the [py:percent](https://jupytext.readthedocs.io/en/latest/formats-scripts.html#the-percent-format) format — plain Python files with cell markers:
105
+
106
+ ```python
107
+ # ---
108
+ # jupyter:
109
+ # kernelspec:
110
+ # name: python3
111
+ # ---
112
+
113
+ # %%
114
+ import numpy as np
115
+
116
+ # %%
117
+ x = np.random.randn(100)
118
+ print(x.mean())
119
+ ```
120
+
121
+ ## Development
122
+
123
+ ```bash
124
+ # install with test dependencies
125
+ uv sync --extra test
126
+
127
+ # run tests (requires a real Jupyter server, started automatically by fixtures)
128
+ uv run pytest -v
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from jupyter_jcli.cli import main
2
+
3
+ main()
@@ -0,0 +1,72 @@
1
+ """jcli — CLI tool for LLM agents to operate Jupyter Lab servers."""
2
+
3
+ import os
4
+ from urllib.parse import urlparse
5
+
6
+ import click
7
+
8
+ from jupyter_jcli.config import get_server_url, get_token
9
+
10
+
11
+ def _ensure_no_proxy(server_url: str) -> None:
12
+ """Ensure local server URLs bypass HTTP proxy."""
13
+ host = urlparse(server_url).hostname or ""
14
+ if host in ("127.0.0.1", "localhost", "::1"):
15
+ no_proxy = os.environ.get("no_proxy", os.environ.get("NO_PROXY", ""))
16
+ if host not in no_proxy:
17
+ new = f"{no_proxy},{host}" if no_proxy else host
18
+ os.environ["no_proxy"] = new
19
+ os.environ["NO_PROXY"] = new
20
+
21
+
22
+ class Context:
23
+ """Shared context passed to all commands."""
24
+
25
+ def __init__(self, server_url: str, token: str | None, use_json: bool):
26
+ self.server_url = server_url
27
+ self.token = token
28
+ self.use_json = use_json
29
+
30
+
31
+ pass_ctx = click.make_pass_decorator(Context)
32
+
33
+
34
+ @click.group()
35
+ @click.option(
36
+ "--server-url", "-s", default=None,
37
+ help="Jupyter server URL (env: JCLI_JUPYTER_SERVER_URL, default: http://localhost:8888)",
38
+ )
39
+ @click.option(
40
+ "--token", "-t", default=None,
41
+ help="Jupyter server token (env: JCLI_JUPYTER_SERVER_TOKEN)",
42
+ )
43
+ @click.option(
44
+ "--json", "-j", "use_json", is_flag=True, default=False,
45
+ help="Output as JSON instead of human-readable text",
46
+ )
47
+ @click.version_option(package_name="jcli")
48
+ @click.pass_context
49
+ def main(ctx, server_url, token, use_json):
50
+ """CLI tool for LLM agents to operate Jupyter Lab servers."""
51
+ resolved_url = get_server_url(server_url)
52
+ _ensure_no_proxy(resolved_url)
53
+ ctx.ensure_object(dict)
54
+ ctx.obj = Context(
55
+ server_url=resolved_url,
56
+ token=get_token(token),
57
+ use_json=use_json,
58
+ )
59
+
60
+
61
+ # Import and register command groups
62
+ from jupyter_jcli.commands.healthcheck import healthcheck # noqa: E402
63
+ from jupyter_jcli.commands.kernelspec import kernelspec # noqa: E402
64
+ from jupyter_jcli.commands.session import session # noqa: E402
65
+ from jupyter_jcli.commands.kernel_cmd import kernel # noqa: E402
66
+ from jupyter_jcli.commands.exec_cmd import exec_cmd # noqa: E402
67
+
68
+ main.add_command(healthcheck)
69
+ main.add_command(kernelspec)
70
+ main.add_command(session)
71
+ main.add_command(kernel)
72
+ main.add_command(exec_cmd, name="exec")
File without changes
@@ -0,0 +1,128 @@
1
+ """jcli exec — execute code or cells from files."""
2
+
3
+ import click
4
+
5
+ from jupyter_jcli.cli import Context, pass_ctx
6
+ from jupyter_jcli.output import emit, emit_error
7
+ from jupyter_jcli.executor import process_outputs, format_outputs_human
8
+ from jupyter_jcli.notebook_writer import write_outputs_to_notebook
9
+
10
+
11
+ @click.command("exec")
12
+ @click.argument("session_id")
13
+ @click.option("--code", "-c", default=None, help="Code to execute directly")
14
+ @click.option("--file", "-f", "file_path", default=None, help="Path to .py or .ipynb file")
15
+ @click.option("--cell", default=None, help="Cell spec: 3, 3:7, 3:, :5 (0-indexed)")
16
+ @click.option("--timeout", default=300, type=int, help="Execution timeout in seconds")
17
+ @pass_ctx
18
+ def exec_cmd(ctx: Context, session_id: str, code: str | None, file_path: str | None, cell: str | None, timeout: int):
19
+ """Execute code in a kernel session.
20
+
21
+ Either --code or --file (with --cell) must be provided.
22
+ When using --file, outputs are automatically written back to the paired .ipynb.
23
+ """
24
+ if not code and not file_path:
25
+ emit_error("PARSE_ERROR", "Either --code or --file must be provided", ctx.use_json)
26
+
27
+ try:
28
+ from jupyter_jcli.server import get_kernel_id_for_session
29
+
30
+ kernel_id = get_kernel_id_for_session(ctx.server_url, session_id, ctx.token)
31
+ except Exception as e:
32
+ emit_error("SESSION_NOT_FOUND", str(e), ctx.use_json)
33
+ return # unreachable but helps type checker
34
+
35
+ # Direct code execution
36
+ if code:
37
+ _exec_code(ctx, kernel_id, code, timeout)
38
+ return
39
+
40
+ # File-based execution
41
+ _exec_file(ctx, kernel_id, file_path, cell, timeout)
42
+
43
+
44
+ def _exec_code(ctx: Context, kernel_id: str, code: str, timeout: int):
45
+ """Execute inline code."""
46
+ try:
47
+ from jupyter_jcli.kernel import execute_code
48
+
49
+ result = execute_code(ctx.server_url, ctx.token, kernel_id, code, timeout)
50
+ raw_outputs = result.get("outputs", [])
51
+ outputs = process_outputs(raw_outputs)
52
+
53
+ if ctx.use_json:
54
+ emit({"status": "ok", "outputs": outputs}, use_json=True)
55
+ else:
56
+ text = format_outputs_human(outputs)
57
+ if text:
58
+ emit({"_human": text}, use_json=False)
59
+
60
+ except Exception as e:
61
+ emit_error("EXECUTION_ERROR", str(e), ctx.use_json)
62
+
63
+
64
+ def _exec_file(ctx: Context, kernel_id: str, file_path: str, cell_spec: str | None, timeout: int):
65
+ """Execute cells from a file."""
66
+ try:
67
+ from jupyter_jcli.parser import parse_file, parse_cell_spec
68
+ from jupyter_jcli.kernel import kernel_connection
69
+
70
+ parsed = parse_file(file_path)
71
+
72
+ # Determine which cells to execute
73
+ code_cells = [c for c in parsed.cells if c.cell_type == "code"]
74
+ if cell_spec:
75
+ indices = parse_cell_spec(cell_spec, len(parsed.cells))
76
+ selected = [c for c in parsed.cells if c.index in indices and c.cell_type == "code"]
77
+ else:
78
+ selected = code_cells
79
+
80
+ if not selected:
81
+ emit_error("PARSE_ERROR", "No code cells found to execute", ctx.use_json)
82
+
83
+ cell_results = []
84
+ all_outputs_human = []
85
+
86
+ with kernel_connection(ctx.server_url, ctx.token, kernel_id) as kernel:
87
+ for cell in selected:
88
+ result = kernel.execute(cell.source, timeout=timeout)
89
+ raw_outputs = result.get("outputs", [])
90
+ outputs = process_outputs(raw_outputs)
91
+
92
+ cell_results.append({
93
+ "cell_index": cell.index,
94
+ "source_preview": cell.source[:80].replace("\n", " "),
95
+ "outputs": outputs,
96
+ "raw_outputs": raw_outputs,
97
+ "execution_count": result.get("execution_count"),
98
+ })
99
+
100
+ if not ctx.use_json:
101
+ all_outputs_human.append(f"--- cell {cell.index} ---")
102
+ text = format_outputs_human(outputs)
103
+ if text:
104
+ all_outputs_human.append(text)
105
+
106
+ # Write back to notebook
107
+ notebook_updated = None
108
+ ipynb_path = parsed.paired_ipynb
109
+ if ipynb_path:
110
+ notebook_updated = write_outputs_to_notebook(ipynb_path, cell_results)
111
+
112
+ if ctx.use_json:
113
+ # Remove raw_outputs from JSON output (they're internal)
114
+ for cr in cell_results:
115
+ del cr["raw_outputs"]
116
+ data = {"status": "ok", "cells": cell_results}
117
+ if notebook_updated:
118
+ data["notebook_updated"] = notebook_updated
119
+ emit(data, use_json=True)
120
+ else:
121
+ if notebook_updated:
122
+ all_outputs_human.append(f"\nNotebook updated: {notebook_updated}")
123
+ emit({"_human": "\n".join(all_outputs_human)}, use_json=False)
124
+
125
+ except SystemExit:
126
+ raise
127
+ except Exception as e:
128
+ emit_error("EXECUTION_ERROR", str(e), ctx.use_json)
@@ -0,0 +1,27 @@
1
+ """jcli healthcheck — check if Jupyter server is reachable."""
2
+
3
+ import click
4
+
5
+ from jupyter_jcli.cli import Context, pass_ctx
6
+ from jupyter_jcli.output import emit, emit_error
7
+
8
+
9
+ @click.command()
10
+ @pass_ctx
11
+ def healthcheck(ctx: Context):
12
+ """Check if the Jupyter server is reachable."""
13
+ try:
14
+ from jupyter_jcli.server import healthcheck as do_healthcheck
15
+
16
+ info = do_healthcheck(ctx.server_url, ctx.token)
17
+ emit(
18
+ {
19
+ "status": "ok",
20
+ "version": info["version"],
21
+ "kernels_running": info["kernels_running"],
22
+ "_human": f"OK Jupyter server v{info['version']} {info['kernels_running']} kernel(s) running",
23
+ },
24
+ use_json=ctx.use_json,
25
+ )
26
+ except Exception as e:
27
+ emit_error("CONNECTION_FAILED", f"Cannot reach Jupyter server at {ctx.server_url}: {e}", ctx.use_json)
@@ -0,0 +1,47 @@
1
+ """jcli kernel — kernel interrupt/restart."""
2
+
3
+ import click
4
+
5
+ from jupyter_jcli.cli import Context, pass_ctx
6
+ from jupyter_jcli.output import emit, emit_error
7
+
8
+
9
+ @click.group()
10
+ def kernel():
11
+ """Manage kernels (interrupt, restart)."""
12
+
13
+
14
+ @kernel.command("interrupt")
15
+ @click.argument("session_id")
16
+ @pass_ctx
17
+ def interrupt(ctx: Context, session_id: str):
18
+ """Interrupt a running kernel by session ID."""
19
+ try:
20
+ from jupyter_jcli.server import get_kernel_id_for_session, interrupt_kernel
21
+
22
+ kernel_id = get_kernel_id_for_session(ctx.server_url, session_id, ctx.token)
23
+ interrupt_kernel(ctx.server_url, kernel_id, ctx.token)
24
+ emit(
25
+ {"status": "ok", "_human": f"Interrupted kernel {kernel_id} (session {session_id})"},
26
+ use_json=ctx.use_json,
27
+ )
28
+ except Exception as e:
29
+ emit_error("KERNEL_NOT_FOUND", str(e), ctx.use_json)
30
+
31
+
32
+ @kernel.command("restart")
33
+ @click.argument("session_id")
34
+ @pass_ctx
35
+ def restart(ctx: Context, session_id: str):
36
+ """Restart a kernel by session ID."""
37
+ try:
38
+ from jupyter_jcli.server import get_kernel_id_for_session, restart_kernel
39
+
40
+ kernel_id = get_kernel_id_for_session(ctx.server_url, session_id, ctx.token)
41
+ restart_kernel(ctx.server_url, kernel_id, ctx.token)
42
+ emit(
43
+ {"status": "ok", "_human": f"Restarted kernel {kernel_id} (session {session_id})"},
44
+ use_json=ctx.use_json,
45
+ )
46
+ except Exception as e:
47
+ emit_error("KERNEL_NOT_FOUND", str(e), ctx.use_json)
@@ -0,0 +1,33 @@
1
+ """jcli kernelspec — kernel spec management."""
2
+
3
+ import click
4
+
5
+ from jupyter_jcli.cli import Context, pass_ctx
6
+ from jupyter_jcli.output import emit, emit_error
7
+
8
+
9
+ @click.group()
10
+ def kernelspec():
11
+ """Manage kernel specifications."""
12
+
13
+
14
+ @kernelspec.command("list")
15
+ @pass_ctx
16
+ def list_specs(ctx: Context):
17
+ """List available kernel specs."""
18
+ try:
19
+ from jupyter_jcli.server import list_kernelspecs
20
+
21
+ specs = list_kernelspecs(ctx.server_url, ctx.token)
22
+
23
+ if ctx.use_json:
24
+ emit({"kernelspecs": specs}, use_json=True)
25
+ else:
26
+ # Table format
27
+ lines = [f"{'NAME':<20} {'DISPLAY_NAME':<20} {'LANGUAGE':<10}"]
28
+ for s in specs:
29
+ lines.append(f"{s['name']:<20} {s['display_name']:<20} {s['language']:<10}")
30
+ emit({"_human": "\n".join(lines)}, use_json=False)
31
+
32
+ except Exception as e:
33
+ emit_error("CONNECTION_FAILED", str(e), ctx.use_json)