jcollab 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,35 @@
1
+ name: publish
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - run: python -m pip install build
19
+ - run: python -m build --outdir dist .
20
+ - uses: actions/upload-artifact@v4
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+
25
+ publish:
26
+ needs: build
27
+ runs-on: ubuntu-latest
28
+ permissions:
29
+ id-token: write
30
+ steps:
31
+ - uses: actions/download-artifact@v4
32
+ with:
33
+ name: dist
34
+ path: dist/
35
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ spike/
5
+ labext/node_modules/
6
+ labext/lib/
7
+ labext/tsconfig.tsbuildinfo
jcollab-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.5
2
+ Name: jcollab
3
+ Version: 0.1.0
4
+ Summary: MCP server for live co-editing and running of JupyterLab notebooks (Yjs, verified writes, multi-instance discovery)
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: jupyter-core
9
+ Requires-Dist: jupyter-nbmodel-client<2,>=1.5.1
10
+ Requires-Dist: jupyter-ydoc<4,>=3.0
11
+ Requires-Dist: mcp<3,>=2
12
+ Requires-Dist: nbformat
13
+ Requires-Dist: pycrdt<0.14,>=0.12.50
14
+ Requires-Dist: websockets>=13
@@ -0,0 +1,37 @@
1
+ # jcollab
2
+
3
+ MCP server (stdio) that lets Claude read, edit and run cells in live JupyterLab notebooks through the shared Yjs
4
+ document, so open browser tabs update immediately and cells look exactly as if the user had run them.
5
+
6
+ * **Discovery, no setup:** finds all running Jupyter servers of the user via `~/.local/share/jupyter/runtime/jpserver-*.json`
7
+ (pid alive + `/api/status` probe). Tools take `instance` (port, `root:port` or root dir name); optional if only one runs.
8
+ * **Verified writes:** every mutating tool reads the file back and reports `persisted`; duplicate cell ids (client/server
9
+ Yjs version skew) are detected and explained.
10
+ * **Execution fidelity:** `[*]` busy state, streamed outputs, kernel execution count, `metadata.execution` timings
11
+ (jupyterlab-execute-time). `run_code` is silent (`store_history=False`), consumes no execution count.
12
+ * **Tools:** list_instances, list_notebooks, read_notebook, read_cell (images), insert_cell, insert_cell_at_start, edit_cell,
13
+ change_cell_type, delete_cell, move_cell, split_cell, merge_cells, get_metadata, set_metadata (cell/notebook),
14
+ run_cell, run_cells, run_code, clear_outputs, inspect_variables, kernel_status, interrupt_kernel, restart_kernel,
15
+ sync_health, user_focus, point_at, find_mentions, add_comment.
16
+
17
+ ## Install
18
+
19
+ From PyPI:
20
+
21
+ pip install jcollab
22
+ claude mcp add --scope user jcollab -- jcollab
23
+
24
+ From source:
25
+
26
+ python3 -m venv .venv && .venv/bin/pip install .
27
+ claude mcp add --scope user jcollab -- $PWD/.venv/bin/jcollab
28
+
29
+ Optional lab extension `labext/` (`jcollab-scroll`): scrolls the user's notebook to the cell `point_at` targets. Install it
30
+ into the interpreter of `jupyter-lab` with `pip install ./labext`, then restart JupyterLab.
31
+
32
+ Requires a JupyterLab with `jupyter-collaboration`. Keep `jupyter-ydoc` of this package on the same major version as the
33
+ server's (pinned `>=3,<4`; see `sync_health`).
34
+
35
+ ## Tests
36
+
37
+ `tests/integration.py [instance]` expects a throwaway JupyterLab (token-less) whose root contains `spike.ipynb`.
@@ -0,0 +1 @@
1
+ {"packageManager": "python", "packageName": "jcollab-scroll", "uninstallInstructions": "pip uninstall jcollab-scroll"}
@@ -0,0 +1,2 @@
1
+ def _jupyter_labextension_paths():
2
+ return [{"src": "labextension", "dest": "jcollab-scroll"}]
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "jcollab-scroll",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "description": "Scroll JupyterLab notebooks to the cell a collaborator (Metis) points at",
6
+ "main": "lib/index.js",
7
+ "files": [
8
+ "lib/**/*.js"
9
+ ],
10
+ "scripts": {
11
+ "build:lib": "tsc",
12
+ "build:labextension": "jupyter labextension build .",
13
+ "build": "npm run build:lib && npm run build:labextension"
14
+ },
15
+ "dependencies": {
16
+ "@jupyter/ydoc": "^3.1.0",
17
+ "@jupyterlab/application": "^4.0.0",
18
+ "@jupyterlab/apputils": "^4.0.0",
19
+ "@jupyterlab/notebook": "^4.0.0"
20
+ },
21
+ "devDependencies": {
22
+ "@jupyterlab/builder": "~4.5.7",
23
+ "typescript": "~5.5.4"
24
+ },
25
+ "jupyterlab": {
26
+ "extension": true,
27
+ "outputDir": "jcollab_scroll/labextension",
28
+ "_build": {
29
+ "load": "static/remoteEntry.01308e5321350a3592c6.js",
30
+ "extension": "./extension"
31
+ }
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+ "use strict";(self.webpackChunkjcollab_scroll=self.webpackChunkjcollab_scroll||[]).push([[509],{509(o,e,t){t.r(e);var n=t(537);function c(o){const e=o&&o.scrollTo;return e&&"string"==typeof e.cell&&"string"==typeof e.nonce?e:null}function l(o){const e=o&&o.user||{};return e.display_name||e.name||"A collaborator"}function a(o,e){return o.content.widgets.findIndex(o=>o.model.id===e)}function s(o,e){const t=o.content,n=t.activeCell;return"edit"===t.mode&&!!n&&!!n.editor&&n.editor.hasFocus()&&t.activeCellIndex!==e}function r(o,e){const t=a(o,e);t>=0&&o.content.scrollToItem(t,"smart").catch(o=>console.warn("jcollab-scroll:",o))}const i={id:"jcollab-scroll:plugin",autoStart:!0,requires:[t(615).INotebookTracker],activate:(o,e)=>{const t=new WeakSet,i=o=>{t.has(o)||(t.add(o),o.context.ready.then(()=>function(o){const e=o.context.model.sharedModel.awareness;if(!e)return void console.warn("jcollab-scroll: no awareness (jupyter-collaboration off?)");const t=new Map;e.getStates().forEach((o,e)=>{const n=c(o);n&&t.set(e,n.nonce)});const i=i=>{for(const d of[...i.added,...i.updated]){if(d===e.clientID)continue;const i=e.getStates().get(d),u=c(i);if(!u||t.get(d)===u.nonce)continue;t.set(d,u.nonce);const f=a(o,u.cell);f<0||(u.force||!s(o,f)?r(o,u.cell):n.Notification.info(`${l(i)} points at cell ${f+1}${u.label?": "+u.label:""}`,{autoClose:1e4,actions:[{label:"Show",callback:()=>r(o,u.cell)}]}))}};e.on("change",i),o.disposed.connect(()=>e.off("change",i))}(o)).catch(()=>{}))};e.forEach(i),e.widgetAdded.connect((o,e)=>i(e)),console.log("jcollab-scroll activated")}};t.d(e,["default",0,i])}}]);
@@ -0,0 +1 @@
1
+ var _JUPYTERLAB;(()=>{"use strict";var e={283(e,t,r){const n={"./index":()=>r.e(509).then(()=>()=>r(509)),"./extension":()=>r.e(509).then(()=>()=>r(509))},o=(e,t)=>(r.R=t,t=r.o(n,e)?n[e]():Promise.resolve().then(()=>{throw new Error('Module "'+e+'" does not exist in container.')}),r.R=void 0,t),i=(e,t)=>{if(!r.S)return;const n="default",o=r.S[n];if(o&&o!==e)throw new Error("Container initialization failed as it has already been initialized with a different share scope");return r.S[n]=e,r.I(n,t)};r.d(t,{get:()=>o,init:()=>i})}};const t={};function r(n){const o=t[n];if(void 0!==o)return o.exports;const i=t[n]={exports:{}};return e[n](i,i.exports,r),i.exports}r.m=e,r.c=t,r.n=e=>{const t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{if(Array.isArray(t))for(var n=0;n<t.length;){var o=t[n++],i=t[n++],l=0===i?{enumerable:!0,value:t[n++]}:{enumerable:!0,get:i};r.o(e,o)||Object.defineProperty(e,o,l)}else for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((t,n)=>(r.f[n](e,t),t),[])),r.u=e=>e+".841856aacf23f4236460.js?v=841856aacf23f4236460",r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{const e={},t="jcollab-scroll:";r.l=(n,o,i,l)=>{if(e[n])return void e[n].push(o);let s,a;if(void 0!==i){const e=document.getElementsByTagName("script");for(var c=0;c<e.length;c++){const r=e[c];if(r.getAttribute("src")==n||r.getAttribute("data-webpack")==t+i){s=r;break}}}s||(a=!0,s=document.createElement("script"),s.charset="utf-8",r.nc&&s.setAttribute("nonce",r.nc),s.setAttribute("data-webpack",t+i),s.src=n),e[n]=[o];const u=(t,r)=>{s.onerror=s.onload=null,clearTimeout(f);const o=e[n];if(delete e[n],s.parentNode?.removeChild(s),o?.forEach(e=>e(r)),t)return t(r)},f=setTimeout(u.bind(null,void 0,{type:"timeout",target:s}),12e4);s.onerror=u.bind(null,s.onerror),s.onload=u.bind(null,s.onload),a&&document.head.appendChild(s)}})(),r.r=e=>{Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{r.S={};const e={},t={};r.I=(n,o)=>{o||(o=[]);let i=t[n];if(i||(i=t[n]={}),o.indexOf(i)>=0)return;if(o.push(i),e[n])return e[n];r.o(r.S,n)||(r.S[n]={});const l=r.S[n],s="jcollab-scroll",a=[];return"default"===n&&((e,t,n,o)=>{const i=l[e]=l[e]||{},a=i[t];(!a||!a.loaded&&(1!=!a.eager?o:s>a.from))&&(i[t]={get:()=>r.e(509).then(()=>()=>r(509)),from:s,eager:!1})})("jcollab-scroll","0.1.0"),e[n]=a.length?Promise.all(a).then(()=>e[n]=1):1}})(),(()=>{let e;r.g.importScripts&&(e=r.g.location+"");const t=r.g.document;if(!e&&t&&("SCRIPT"===t.currentScript?.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){const r=t.getElementsByTagName("script");if(r.length){let t=r.length-1;for(;t>-1&&(!e||!/^https?:/.test(e));)e=r[t--].src}}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:|[?#].*$/g,"").replace(/\/[^/]+$/,"/"),r.p=e})(),(()=>{var e=e=>{var t=e=>e.split(".").map(e=>+e==e?+e:e),r=/^([^-+]+)?(?:-([^+]+))?(?:\+(.+))?$/.exec(e),n=r[1]?t(r[1]):[];return r[2]&&(n.length++,n.push.apply(n,t(r[2]))),r[3]&&(n.push([]),n.push.apply(n,t(r[3]))),n},t=e=>{var r=e[0],n="";if(1===e.length)return"*";if(r+.5){n+=0==r?">=":-1==r?"<":1==r?"^":2==r?"~":r>0?"=":"!=";for(var o=1,i=1;i<e.length;i++)o--,n+="u"==(typeof(s=e[i]))[0]?"-":(o>0?".":"")+(o=2,s);return n}var l=[];for(i=1;i<e.length;i++){var s=e[i];l.push(0===s?"not("+a()+")":1===s?"("+a()+" || "+a()+")":2===s?l.pop()+" "+l.pop():t(s))}return a();function a(){return l.pop().replace(/^\((.+)\)$/,"$1")}},n=(t,r)=>{if(0 in t){r=e(r);var o=t[0],i=o<0;i&&(o=-o-1);for(var l=0,s=1,a=!0;;s++,l++){var c,u,f=s<t.length?(typeof t[s])[0]:"";if(l>=r.length||"o"==(u=(typeof(c=r[l]))[0]))return!a||("u"==f?s>o&&!i:""==f!=i);if("u"==u){if(!a||"u"!=f)return!1}else if(a)if(f==u)if(s<=o){if(c!=t[s])return!1}else{if(i?c>t[s]:c<t[s])return!1;c!=t[s]&&(a=!1)}else if("s"!=f&&"n"!=f){if(i||s<=o)return!1;a=!1,s--}else{if(s<=o||u<f!=i)return!1;a=!1}else"s"!=f&&"n"!=f&&(a=!1,s--)}}var d=[],p=d.pop.bind(d);for(l=1;l<t.length;l++){var h=t[l];d.push(1==h?p()|p():2==h?p()&p():h?n(h,r):!p())}return!!p()};const o=(t,r,n)=>{const o=n?(e=>Object.keys(e).reduce((t,r)=>(e[r].eager&&(t[r]=e[r]),t),{}))(t[r]):t[r];return Object.keys(o).reduce((t,r)=>!t||!o[t].loaded&&((t,r)=>{t=e(t),r=e(r);for(var n=0;;){if(n>=t.length)return n<r.length&&"u"!=(typeof r[n])[0];var o=t[n],i=(typeof o)[0];if(n>=r.length)return"u"==i;var l=r[n],s=(typeof l)[0];if(i!=s)return"o"==i&&"n"==s||"s"==s||"u"==i;if("o"!=i&&"u"!=i&&o!=l)return o<l;n++}})(t,r)?r:t,0)},i=(e,t,r)=>r?r():((e,t)=>(e=>{throw new Error(e)})("Shared module "+t+" doesn't exist in shared scope "+e))(e,t),l=(e=>function(t,n,o,i,l){const s=r.I(t);return s?.then&&!o?s.then(e.bind(e,t,r.S[t],n,!1,i,l)):e(t,r.S[t],n,o,i,l)})((e,l,s,a,c,u)=>{if(!((e,t)=>e&&r.o(e,t))(l,s))return i(e,s,u);const f=o(l,s,a);return n(c,f)||(p=((e,r,n,o)=>"Unsatisfied version "+n+" from "+(n&&e[r][n].from)+" of shared singleton module "+r+" (required "+t(o)+")")(l,s,f,c),"undefined"!=typeof console&&console.warn&&console.warn(p)),(d=l[s][f]).loaded=1,d.get();var d,p}),s={},a={537:()=>l("default","@jupyterlab/apputils",!1,[1,4,6,7]),615:()=>l("default","@jupyterlab/notebook",!1,[1,4,5,7])},c={509:[537,615]},u={};r.f.consumes=(e,t)=>{r.o(c,e)&&c[e].forEach(e=>{if(r.o(s,e))return t.push(s[e]);if(!u[e]){const n=t=>{s[e]=0,r.m[e]=n=>{delete r.c[e],n.exports=t()}};u[e]=!0;const o=t=>{delete s[e],r.m[e]=n=>{throw delete r.c[e],t}};try{const r=a[e]();r.then?t.push(s[e]=r.then(n).catch(o)):n(r)}catch(e){o(e)}}})}})(),(()=>{const e={2:0};r.f.j=(t,n)=>{let o=r.o(e,t)?e[t]:void 0;if(0!==o)if(o)n.push(o[2]);else{const i=new Promise((r,n)=>o=e[t]=[r,n]);n.push(o[2]=i);const l=new Error,s=n=>{if(r.o(e,t)&&(o=e[t],0!==o&&(e[t]=void 0),o)){const e=n&&("load"===n.type?"missing":n.type),r=n&&n.target&&n.target.src;l.message="Loading chunk "+t+" failed.\n("+e+": "+r+")",l.name="ChunkLoadError",l.type=e,l.request=r,l.event=n,o[1](l)}};r.l(r.p+r.u(t),s,"chunk-"+t,t)}};const t=(t,n)=>{let[o,i,l]=n;var s,a,c=0;if(o.some(t=>0!==e[t])){for(s in i)r.o(i,s)&&(r.m[s]=i[s]);l&&l(r)}for(t&&t(n);c<o.length;c++)a=o[c],r.o(e,a)&&e[a]&&e[a][0](),e[a]=0},n=self.webpackChunkjcollab_scroll=self.webpackChunkjcollab_scroll||[];n.forEach(t.bind(null,0)),n.push=t.bind(null,n.push.bind(n))})();let n=r(283);(_JUPYTERLAB=void 0===_JUPYTERLAB?{}:_JUPYTERLAB)["jcollab-scroll"]=n})();
@@ -0,0 +1,4 @@
1
+ /* This is a generated file of CSS imports */
2
+ /* It was generated by @jupyterlab/builder in Build.ensureAssets() */
3
+
4
+