ttsd-colabcli 1.0.0

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 (47) hide show
  1. package/cli.js +148 -0
  2. package/core/app/__init__.py +0 -0
  3. package/core/app/colab_cli/__init__.py +0 -0
  4. package/core/app/colab_cli/__pycache__/__init__.cpython-312.pyc +0 -0
  5. package/core/app/colab_cli/__pycache__/auth.cpython-312.pyc +0 -0
  6. package/core/app/colab_cli/__pycache__/auto_update.cpython-312.pyc +0 -0
  7. package/core/app/colab_cli/__pycache__/cli.cpython-312.pyc +0 -0
  8. package/core/app/colab_cli/__pycache__/client.cpython-312.pyc +0 -0
  9. package/core/app/colab_cli/__pycache__/common.cpython-312.pyc +0 -0
  10. package/core/app/colab_cli/__pycache__/console.cpython-312.pyc +0 -0
  11. package/core/app/colab_cli/__pycache__/contents.cpython-312.pyc +0 -0
  12. package/core/app/colab_cli/__pycache__/history.cpython-312.pyc +0 -0
  13. package/core/app/colab_cli/__pycache__/runtime.cpython-312.pyc +0 -0
  14. package/core/app/colab_cli/__pycache__/state.cpython-312.pyc +0 -0
  15. package/core/app/colab_cli/__pycache__/utils.cpython-312.pyc +0 -0
  16. package/core/app/colab_cli/auth.py +278 -0
  17. package/core/app/colab_cli/auto_update.py +248 -0
  18. package/core/app/colab_cli/cli.py +155 -0
  19. package/core/app/colab_cli/client.py +310 -0
  20. package/core/app/colab_cli/commands/__init__.py +14 -0
  21. package/core/app/colab_cli/commands/__pycache__/__init__.cpython-312.pyc +0 -0
  22. package/core/app/colab_cli/commands/__pycache__/automation.cpython-312.pyc +0 -0
  23. package/core/app/colab_cli/commands/__pycache__/execution.cpython-312.pyc +0 -0
  24. package/core/app/colab_cli/commands/__pycache__/files.cpython-312.pyc +0 -0
  25. package/core/app/colab_cli/commands/__pycache__/run.cpython-312.pyc +0 -0
  26. package/core/app/colab_cli/commands/__pycache__/session.cpython-312.pyc +0 -0
  27. package/core/app/colab_cli/commands/__pycache__/utility.cpython-312.pyc +0 -0
  28. package/core/app/colab_cli/commands/automation.py +265 -0
  29. package/core/app/colab_cli/commands/execution.py +362 -0
  30. package/core/app/colab_cli/commands/files.py +204 -0
  31. package/core/app/colab_cli/commands/run.py +477 -0
  32. package/core/app/colab_cli/commands/session.py +519 -0
  33. package/core/app/colab_cli/commands/utility.py +436 -0
  34. package/core/app/colab_cli/common.py +185 -0
  35. package/core/app/colab_cli/console.py +172 -0
  36. package/core/app/colab_cli/contents.py +93 -0
  37. package/core/app/colab_cli/converter.py +184 -0
  38. package/core/app/colab_cli/history.py +65 -0
  39. package/core/app/colab_cli/oauth_config.json +11 -0
  40. package/core/app/colab_cli/repl.py +173 -0
  41. package/core/app/colab_cli/runtime.py +262 -0
  42. package/core/app/colab_cli/state.py +156 -0
  43. package/core/app/colab_cli/utils.py +85 -0
  44. package/core/colab/worker.py +679 -0
  45. package/core/daemon.py +184 -0
  46. package/core/requirements.txt +8 -0
  47. package/package.json +22 -0
@@ -0,0 +1,362 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import datetime
16
+ import nbformat
17
+ import os
18
+ import re
19
+ import sys
20
+ import typer
21
+ import uuid
22
+ from nbformat.v4 import new_output
23
+ from typing import Optional
24
+ from typing_extensions import Annotated
25
+
26
+ from app.colab_cli.runtime import ColabRuntime
27
+ from app.colab_cli.utils import handle_image, is_terminal_error
28
+ from app.colab_cli.console import connect_console
29
+
30
+ TITLE_REGEX = re.compile(r"^\s*#\s*@title\s+(.*)", re.MULTILINE)
31
+
32
+
33
+ def is_stdin_tty():
34
+ return sys.stdin.isatty()
35
+
36
+
37
+ def save_output(outputs, cell):
38
+ if cell is None:
39
+ return
40
+
41
+ if not hasattr(cell, "outputs"):
42
+ cell.outputs = []
43
+ else:
44
+ cell.outputs.clear()
45
+
46
+ for out in outputs:
47
+ if out.get("output_type") == "stream":
48
+ cell.outputs.append(
49
+ new_output(
50
+ output_type="stream",
51
+ name=out.get("name", "stdout"),
52
+ text=out.get("text", ""),
53
+ )
54
+ )
55
+ elif "data" in out:
56
+ output_type = out.get("output_type", "display_data")
57
+ cell.outputs.append(
58
+ new_output(
59
+ output_type=output_type,
60
+ data=out["data"],
61
+ metadata=out.get("metadata", {}),
62
+ )
63
+ )
64
+ elif out.get("output_type") == "error":
65
+ cell.outputs.append(
66
+ new_output(
67
+ output_type="error",
68
+ ename=out.get("ename", "Error"),
69
+ evalue=out.get("evalue", ""),
70
+ traceback=out.get("traceback", []),
71
+ )
72
+ )
73
+
74
+
75
+ def display_output(out, output_image=None):
76
+ if out.get("output_type") == "stream":
77
+ stream = sys.stderr if out.get("name") == "stderr" else sys.stdout
78
+ stream.write(out.get("text", ""))
79
+ stream.flush()
80
+ elif "data" in out:
81
+ data = out["data"]
82
+ if text := data.get("text/plain"):
83
+ typer.echo(text)
84
+ if png := data.get("image/png"):
85
+ handle_image(png, "image/png", target_path=output_image)
86
+ elif jpeg := data.get("image/jpeg"):
87
+ handle_image(jpeg, "image/jpeg", target_path=output_image)
88
+ elif out.get("output_type") == "error":
89
+ tb = out.get("traceback", [])
90
+ if tb:
91
+ sys.stderr.write("".join(tb) + "\n")
92
+ else:
93
+ ename = out.get("ename", "Error")
94
+ evalue = out.get("evalue", "")
95
+ sys.stderr.write(f"{ename}: {evalue}\n")
96
+ else:
97
+ # Ignore silent outputs like metadata or clear_output for streaming
98
+ pass
99
+
100
+
101
+ def exec_command(
102
+ session: Annotated[
103
+ Optional[str], typer.Option("-s", "--session", help="Session name")
104
+ ] = None,
105
+ file: Annotated[
106
+ Optional[str], typer.Option("-f", "--file", help="File to execute")
107
+ ] = None,
108
+ output_image: Annotated[
109
+ Optional[str], typer.Option("--output-image", help="Path to save plot")
110
+ ] = None,
111
+ timeout: Annotated[
112
+ Optional[float],
113
+ typer.Option("--timeout", help="Timeout in seconds for code execution"),
114
+ ] = 30.0,
115
+ ):
116
+ """Execute code in a session"""
117
+ from app.colab_cli.common import state
118
+
119
+ name = state.resolve_session(session)
120
+ s = state.store.get(name)
121
+ if not s:
122
+ typer.echo(f"[colab] Session '{name}' not found.")
123
+ raise typer.Exit(1)
124
+
125
+ code_blocks = []
126
+ if file:
127
+ if file.endswith(".ipynb"):
128
+ typer.echo(f"[colab] Parsing notebook '{file}'...")
129
+ with open(file, "r", encoding="utf-8") as f:
130
+ nb = nbformat.read(f, as_version=4)
131
+ for cell in nb.cells:
132
+ # nbformat v4.5+ requires 'id' at the top level
133
+ if not hasattr(cell, "id") or not cell.id:
134
+ cell.id = str(uuid.uuid4())
135
+
136
+ if cell.cell_type == "code":
137
+ code_blocks.append(
138
+ {"code": cell.source, "id": cell.id, "cell": cell}
139
+ )
140
+ else:
141
+ with open(file, "r") as f:
142
+ code_blocks.append({"code": f.read(), "id": None})
143
+ else:
144
+ if is_stdin_tty():
145
+ typer.echo("[colab] Error: No input provided. Pipe code or provide a file.")
146
+ raise typer.Exit(1)
147
+ code_blocks.append({"code": sys.stdin.read(), "id": None})
148
+
149
+ if not any(b["code"].strip() for b in code_blocks):
150
+ raise typer.Exit(0)
151
+
152
+ def on_started(kid):
153
+ s.kernel_id = kid
154
+ state.store.add(s)
155
+
156
+ def on_sess_started(sid):
157
+ s.session_id = sid
158
+ state.store.add(s)
159
+
160
+ runtime = ColabRuntime(
161
+ s.url,
162
+ s.token,
163
+ kernel_id=s.kernel_id,
164
+ session_id=s.session_id,
165
+ on_kernel_started=on_started,
166
+ on_session_started=on_sess_started,
167
+ )
168
+ try:
169
+ # Ensure we are in /content which is the standard Colab working directory
170
+ runtime.execute_code(
171
+ "import os; os.makedirs('/content', exist_ok=True); os.chdir('/content')"
172
+ )
173
+ except Exception as e:
174
+ if is_terminal_error(e):
175
+ typer.echo(
176
+ f"[colab] Session '{name}' appears to be lost (404/401). Cleaning up."
177
+ )
178
+ state.prune_session(name)
179
+ raise typer.Exit(1)
180
+ raise e
181
+
182
+ try:
183
+ is_nb = file and file.endswith(".ipynb")
184
+ s.running = f"exec({file or 'stdin'})"
185
+ state.store.add(s)
186
+
187
+ for i, block in enumerate(code_blocks):
188
+ code = block["code"]
189
+ identifier = None
190
+ if is_nb:
191
+ title_match = TITLE_REGEX.search(code)
192
+ if title_match:
193
+ identifier = title_match.group(1).strip()
194
+ elif block.get("id"):
195
+ identifier = block["id"]
196
+ else:
197
+ identifier = ""
198
+
199
+ identifier_str = f" - {identifier}" if identifier else ""
200
+ typer.echo(
201
+ f"[colab] Executing cell {i + 1}/{len(code_blocks)}{identifier_str}..."
202
+ )
203
+
204
+ s.last_execution = (
205
+ file or "stdin",
206
+ identifier,
207
+ datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
208
+ )
209
+ state.store.add(s)
210
+
211
+ outputs = runtime.execute_code(
212
+ code,
213
+ output_hook=lambda o: display_output(o, output_image),
214
+ timeout=timeout,
215
+ )
216
+ if "cell" in block:
217
+ save_output(outputs, block["cell"])
218
+ state.history.log_event(
219
+ name,
220
+ "execution",
221
+ {
222
+ "code": code,
223
+ "outputs": outputs,
224
+ "cell_index": i if len(code_blocks) > 1 else None,
225
+ "cell_id": block.get("id"),
226
+ },
227
+ )
228
+ finally:
229
+ s.running = None
230
+ state.store.add(s)
231
+ runtime.stop()
232
+ if file and file.endswith(".ipynb"):
233
+ output_file = os.path.splitext(file)[0] + "_output.ipynb"
234
+ typer.echo(f"[colab] Saving notebook with outputs to '{output_file}'...")
235
+ with open(output_file, "w", encoding="utf-8") as f:
236
+ nbformat.write(nb, f)
237
+
238
+
239
+ def repl(
240
+ session: Annotated[
241
+ Optional[str], typer.Option("-s", "--session", help="Session name")
242
+ ] = None,
243
+ output_image: Annotated[
244
+ Optional[str], typer.Option("--output-image", help="Path to save plot")
245
+ ] = None,
246
+ ):
247
+ """Start an interactive REPL"""
248
+ from app.colab_cli.common import state
249
+
250
+ name = state.resolve_session(session)
251
+ s = state.store.get(name)
252
+ if not s:
253
+ typer.echo(f"[colab] Session '{name}' not found.")
254
+ raise typer.Exit(1)
255
+
256
+ def on_started(kid):
257
+ s.kernel_id = kid
258
+ state.store.add(s)
259
+
260
+ def on_sess_started(sid):
261
+ s.session_id = sid
262
+ state.store.add(s)
263
+
264
+ runtime = ColabRuntime(
265
+ s.url,
266
+ s.token,
267
+ kernel_id=s.kernel_id,
268
+ session_id=s.session_id,
269
+ on_kernel_started=on_started,
270
+ on_session_started=on_sess_started,
271
+ )
272
+ try:
273
+ # Ensure we are in /content which is the standard Colab working directory
274
+ runtime.execute_code(
275
+ "import os; os.makedirs('/content', exist_ok=True); os.chdir('/content')"
276
+ )
277
+ except Exception as e:
278
+ if is_terminal_error(e):
279
+ typer.echo(
280
+ f"[colab] Session '{name}' appears to be lost (404/401). Cleaning up."
281
+ )
282
+ state.prune_session(name)
283
+ raise typer.Exit(1)
284
+ raise e
285
+
286
+ if not is_stdin_tty():
287
+ code = sys.stdin.read()
288
+ if not code.strip():
289
+ raise typer.Exit(0)
290
+
291
+ s.last_execution = (
292
+ "stdin",
293
+ None,
294
+ datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
295
+ )
296
+ s.running = "repl(stdin)"
297
+ state.store.add(s)
298
+ try:
299
+ outputs = runtime.execute_code(
300
+ code, output_hook=lambda o: display_output(o, output_image)
301
+ )
302
+ state.history.log_event(
303
+ name, "execution", {"code": code, "outputs": outputs, "source": "piped"}
304
+ )
305
+ finally:
306
+ s.running = None
307
+ state.store.add(s)
308
+ runtime.stop()
309
+ else:
310
+ from app.colab_cli.repl import ColabREPL
311
+
312
+ s.running = "repl"
313
+ state.store.add(s)
314
+ try:
315
+ repl_inst = ColabREPL(
316
+ runtime,
317
+ session_name=s.name,
318
+ history_logger=state.history,
319
+ output_image=output_image,
320
+ )
321
+ state.history.log_event(name, "repl_started", {})
322
+ repl_inst.run()
323
+ finally:
324
+ s.running = None
325
+ state.store.add(s)
326
+
327
+
328
+ def console(
329
+ session: Annotated[
330
+ Optional[str], typer.Option("-s", "--session", help="Session name")
331
+ ] = None,
332
+ ):
333
+ """Connect to raw TTY console"""
334
+ from app.colab_cli.common import state
335
+
336
+ name = state.resolve_session(session)
337
+ s = state.store.get(name)
338
+ if not s:
339
+ typer.echo(f"[colab] Session '{name}' not found.")
340
+ raise typer.Exit(1)
341
+ state.history.log_event(s.name, "console_started", {})
342
+ s.running = "console"
343
+ state.store.add(s)
344
+ try:
345
+ connect_console(s)
346
+ except Exception as e:
347
+ if is_terminal_error(e):
348
+ typer.echo(
349
+ f"[colab] Session '{name}' appears to be lost (404/401). Cleaning up."
350
+ )
351
+ state.prune_session(name)
352
+ raise typer.Exit(1)
353
+ raise e
354
+ finally:
355
+ s.running = None
356
+ state.store.add(s)
357
+
358
+
359
+ def register(app: typer.Typer):
360
+ app.command(name="exec")(exec_command)
361
+ app.command()(repl)
362
+ app.command()(console)
@@ -0,0 +1,204 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import click
16
+ import hashlib
17
+ import os
18
+ import tempfile
19
+ import typer
20
+ from typing import Optional
21
+ from typing_extensions import Annotated
22
+
23
+ from app.colab_cli.contents import ContentsClient
24
+
25
+
26
+ def ls(
27
+ session: Annotated[
28
+ Optional[str], typer.Option("-s", "--session", help="Session name")
29
+ ] = None,
30
+ path: Annotated[str, typer.Argument(help="Remote path to list")] = "content",
31
+ ):
32
+ """List files in a session"""
33
+ from app.colab_cli.common import state
34
+
35
+ name = state.resolve_session(session)
36
+ s = state.store.get(name)
37
+ if not s:
38
+ typer.echo(f"[colab] Session '{name}' not found.")
39
+ raise typer.Exit(1)
40
+ contents = ContentsClient(s)
41
+ try:
42
+ data = contents.list_dir(path)
43
+ state.history.log_event(name, "file_operation", {"op": "ls", "path": path})
44
+ if data.get("type") == "directory":
45
+ items = data.get("content", [])
46
+ for item in sorted(
47
+ items, key=lambda x: (x.get("type") != "directory", x.get("name"))
48
+ ):
49
+ suffix = "/" if item.get("type") == "directory" else ""
50
+ typer.echo(f"{item.get('name')}{suffix}")
51
+ else:
52
+ typer.echo(data.get("name"))
53
+ except Exception as e:
54
+ typer.echo(f"[colab] Error: {e}")
55
+ raise typer.Exit(1)
56
+
57
+
58
+ def rm(
59
+ session: Annotated[
60
+ Optional[str], typer.Option("-s", "--session", help="Session name")
61
+ ] = None,
62
+ path: Annotated[str, typer.Argument(help="Remote path to remove")] = ...,
63
+ ):
64
+ """Remove a remote file"""
65
+ from app.colab_cli.common import state
66
+
67
+ name = state.resolve_session(session)
68
+ s = state.store.get(name)
69
+ if not s:
70
+ typer.echo(f"[colab] Session '{name}' not found.")
71
+ raise typer.Exit(1)
72
+ contents = ContentsClient(s)
73
+ try:
74
+ contents.rm(path)
75
+ state.history.log_event(name, "file_operation", {"op": "rm", "path": path})
76
+ typer.echo(f"[colab] Deleted {path}")
77
+ except Exception as e:
78
+ typer.echo(f"[colab] Error: {e}")
79
+ raise typer.Exit(1)
80
+
81
+
82
+ def upload(
83
+ session: Annotated[
84
+ Optional[str], typer.Option("-s", "--session", help="Session name")
85
+ ] = None,
86
+ local_path: Annotated[str, typer.Argument(help="Local file to upload")] = ...,
87
+ remote_path: Annotated[str, typer.Argument(help="Remote path to upload to")] = ...,
88
+ ):
89
+ """Upload a file to a session"""
90
+ from app.colab_cli.common import state
91
+
92
+ name = state.resolve_session(session)
93
+ s = state.store.get(name)
94
+ if not s:
95
+ typer.echo(f"[colab] Session '{name}' not found.")
96
+ raise typer.Exit(1)
97
+ if not os.path.isfile(local_path):
98
+ typer.echo(f"[colab] Local file '{local_path}' not found.")
99
+ raise typer.Exit(1)
100
+ contents = ContentsClient(s)
101
+ try:
102
+ contents.upload(local_path, remote_path)
103
+ state.history.log_event(
104
+ name,
105
+ "file_operation",
106
+ {"op": "upload", "local": local_path, "remote": remote_path},
107
+ )
108
+ typer.echo(f"[colab] Uploaded '{local_path}' to '{remote_path}'")
109
+ except Exception as e:
110
+ typer.echo(f"[colab] Upload failed: {e}")
111
+ raise typer.Exit(1)
112
+
113
+
114
+ def download(
115
+ session: Annotated[
116
+ Optional[str], typer.Option("-s", "--session", help="Session name")
117
+ ] = None,
118
+ remote_path: Annotated[
119
+ str, typer.Argument(help="Remote path to download from")
120
+ ] = ...,
121
+ local_path: Annotated[
122
+ str, typer.Argument(help="Local path to save the file")
123
+ ] = ...,
124
+ ):
125
+ """Download a file from a session"""
126
+ from app.colab_cli.common import state
127
+
128
+ name = state.resolve_session(session)
129
+ s = state.store.get(name)
130
+ if not s:
131
+ typer.echo(f"[colab] Session '{name}' not found.")
132
+ raise typer.Exit(1)
133
+ contents = ContentsClient(s)
134
+ try:
135
+ contents.download(remote_path, local_path)
136
+ state.history.log_event(
137
+ name,
138
+ "file_operation",
139
+ {"op": "download", "remote": remote_path, "local": local_path},
140
+ )
141
+ typer.echo(f"[colab] Downloaded '{remote_path}' to '{local_path}'")
142
+ except Exception as e:
143
+ typer.echo(f"[colab] Download failed: {e}")
144
+ raise typer.Exit(1)
145
+
146
+
147
+ def edit(
148
+ session: Annotated[
149
+ Optional[str], typer.Option("-s", "--session", help="Session name")
150
+ ] = None,
151
+ remote_path: Annotated[str, typer.Argument(help="Remote path to edit")] = ...,
152
+ ):
153
+ """Edit a file on a running Colab session"""
154
+ from app.colab_cli.common import state
155
+
156
+ name = state.resolve_session(session)
157
+ s = state.store.get(name)
158
+ if not s:
159
+ typer.echo(f"[colab] Session '{name}' not found.")
160
+ raise typer.Exit(1)
161
+
162
+ contents = ContentsClient(s)
163
+
164
+ def get_file_hash(path):
165
+ if not os.path.exists(path):
166
+ return None
167
+ with open(path, "rb") as f:
168
+ return hashlib.file_digest(f, "sha256").hexdigest()
169
+
170
+ _, ext = os.path.splitext(remote_path)
171
+
172
+ with tempfile.NamedTemporaryFile(suffix=ext) as tf:
173
+ local_path = tf.name
174
+
175
+ try:
176
+ contents.download(remote_path, local_path)
177
+ except Exception:
178
+ # If download fails, assume file doesn't exist and start empty
179
+ pass
180
+
181
+ hash_before = get_file_hash(local_path)
182
+
183
+ click.edit(filename=local_path)
184
+
185
+ hash_after = get_file_hash(local_path)
186
+
187
+ if hash_after != hash_before:
188
+ contents.upload(local_path, remote_path)
189
+ state.history.log_event(
190
+ name,
191
+ "file_operation",
192
+ {"op": "edit", "remote": remote_path},
193
+ )
194
+ typer.echo(f"[colab] Edited and uploaded '{remote_path}'")
195
+ else:
196
+ typer.echo(f"[colab] No changes made to '{remote_path}'")
197
+
198
+
199
+ def register(app: typer.Typer):
200
+ app.command()(ls)
201
+ app.command()(rm)
202
+ app.command()(upload)
203
+ app.command()(download)
204
+ app.command()(edit)