ctxsync 0.8.0__py3-none-any.whl
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.
- ctxsync/__init__.py +0 -0
- ctxsync/chat_sync.py +186 -0
- ctxsync/cli/__init__.py +3 -0
- ctxsync/cli/auth.py +77 -0
- ctxsync/cli/category.py +71 -0
- ctxsync/cli/chat.py +357 -0
- ctxsync/cli/config.py +72 -0
- ctxsync/cli/file.py +29 -0
- ctxsync/cli/main.py +257 -0
- ctxsync/cli/organization.py +98 -0
- ctxsync/cli/project.py +422 -0
- ctxsync/cli/session.py +626 -0
- ctxsync/cli/submodule.py +148 -0
- ctxsync/cli/sync.py +79 -0
- ctxsync/compression.py +302 -0
- ctxsync/configmanager/__init__.py +5 -0
- ctxsync/configmanager/base_config_manager.py +255 -0
- ctxsync/configmanager/file_config_manager.py +362 -0
- ctxsync/configmanager/inmemory_config_manager.py +134 -0
- ctxsync/exceptions.py +22 -0
- ctxsync/provider_factory.py +38 -0
- ctxsync/providers/__init__.py +0 -0
- ctxsync/providers/base_claude_ai.py +537 -0
- ctxsync/providers/base_provider.py +109 -0
- ctxsync/providers/claude_ai.py +192 -0
- ctxsync/session_key_manager.py +129 -0
- ctxsync/syncmanager.py +328 -0
- ctxsync/utils.py +416 -0
- ctxsync-0.8.0.dist-info/METADATA +151 -0
- ctxsync-0.8.0.dist-info/RECORD +34 -0
- ctxsync-0.8.0.dist-info/WHEEL +5 -0
- ctxsync-0.8.0.dist-info/entry_points.txt +2 -0
- ctxsync-0.8.0.dist-info/licenses/LICENSE +21 -0
- ctxsync-0.8.0.dist-info/top_level.txt +1 -0
ctxsync/cli/session.py
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from ..utils import handle_errors, validate_and_get_provider
|
|
4
|
+
from ..exceptions import ProviderError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group()
|
|
8
|
+
def session():
|
|
9
|
+
"""Manage Claude Code web sessions."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@session.group()
|
|
14
|
+
def environment():
|
|
15
|
+
"""Manage Claude Code environments."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@session.group()
|
|
20
|
+
def branch():
|
|
21
|
+
"""Manage Claude Code repository branches."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@branch.command(name="ls")
|
|
26
|
+
@click.option(
|
|
27
|
+
"--json",
|
|
28
|
+
"json_output",
|
|
29
|
+
is_flag=True,
|
|
30
|
+
help="Output in JSON format",
|
|
31
|
+
)
|
|
32
|
+
@click.option(
|
|
33
|
+
"-s",
|
|
34
|
+
"--search",
|
|
35
|
+
help="Filter repositories by name",
|
|
36
|
+
)
|
|
37
|
+
@click.pass_obj
|
|
38
|
+
@handle_errors
|
|
39
|
+
def branch_ls(config, json_output, search):
|
|
40
|
+
"""List available repositories for Claude Code sessions."""
|
|
41
|
+
import json as json_module
|
|
42
|
+
|
|
43
|
+
provider = validate_and_get_provider(config)
|
|
44
|
+
active_organization_id = config.get("active_organization_id")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
repos_data = provider.get_code_repos(active_organization_id)
|
|
48
|
+
|
|
49
|
+
if not repos_data or not repos_data.get("repos"):
|
|
50
|
+
click.echo("No repositories found.")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
repos = repos_data["repos"]
|
|
54
|
+
|
|
55
|
+
# Filter by search term if provided
|
|
56
|
+
if search:
|
|
57
|
+
repos = [
|
|
58
|
+
r
|
|
59
|
+
for r in repos
|
|
60
|
+
if search.lower() in r.get("repo", {}).get("name", "").lower()
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
if not repos:
|
|
64
|
+
click.echo(f"No repositories found matching '{search}'.")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if json_output:
|
|
68
|
+
click.echo(json_module.dumps(repos, indent=2))
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Display repositories in a formatted way
|
|
72
|
+
click.echo(f"Found {len(repos)} repository(ies):")
|
|
73
|
+
|
|
74
|
+
for idx, repo_data in enumerate(repos, 1):
|
|
75
|
+
repo = repo_data.get("repo", {})
|
|
76
|
+
name = repo.get("name", "Unknown")
|
|
77
|
+
owner = repo.get("owner", {}).get("login", "Unknown")
|
|
78
|
+
default_branch = repo.get("default_branch", "N/A")
|
|
79
|
+
|
|
80
|
+
click.echo(f"\n{idx}. {owner}/{name}")
|
|
81
|
+
click.echo(f" Default branch: {default_branch}")
|
|
82
|
+
|
|
83
|
+
except ProviderError as e:
|
|
84
|
+
click.echo(f"Failed to list repositories: {str(e)}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@environment.command(name="ls")
|
|
88
|
+
@click.option(
|
|
89
|
+
"--json",
|
|
90
|
+
"json_output",
|
|
91
|
+
is_flag=True,
|
|
92
|
+
help="Output in JSON format",
|
|
93
|
+
)
|
|
94
|
+
@click.pass_obj
|
|
95
|
+
@handle_errors
|
|
96
|
+
def environment_ls(config, json_output):
|
|
97
|
+
"""List all Claude Code environments."""
|
|
98
|
+
import json as json_module
|
|
99
|
+
|
|
100
|
+
provider = validate_and_get_provider(config)
|
|
101
|
+
active_organization_id = config.get("active_organization_id")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
environments_data = provider.get_environments(active_organization_id)
|
|
105
|
+
|
|
106
|
+
if not environments_data or not environments_data.get("environments"):
|
|
107
|
+
click.echo("No environments found.")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
environments = environments_data["environments"]
|
|
111
|
+
|
|
112
|
+
if json_output:
|
|
113
|
+
click.echo(json_module.dumps(environments, indent=2))
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Display environments in a formatted way
|
|
117
|
+
click.echo(f"Found {len(environments)} environment(s):")
|
|
118
|
+
|
|
119
|
+
for idx, env in enumerate(environments, 1):
|
|
120
|
+
env_id = env.get("environment_id", "N/A")
|
|
121
|
+
name = env.get("name", "Unnamed Environment")
|
|
122
|
+
kind = env.get("kind", "N/A")
|
|
123
|
+
state = env.get("state", "unknown")
|
|
124
|
+
|
|
125
|
+
click.echo(f"\n{idx}. {name}")
|
|
126
|
+
click.echo(f" ID: {env_id}")
|
|
127
|
+
click.echo(f" Kind: {kind}")
|
|
128
|
+
click.echo(f" State: {state}")
|
|
129
|
+
|
|
130
|
+
except ProviderError as e:
|
|
131
|
+
click.echo(f"Failed to list environments: {str(e)}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@session.command()
|
|
135
|
+
@click.option(
|
|
136
|
+
"-a",
|
|
137
|
+
"--all",
|
|
138
|
+
"archive_all",
|
|
139
|
+
is_flag=True,
|
|
140
|
+
help="Archive all active sessions",
|
|
141
|
+
)
|
|
142
|
+
@click.option(
|
|
143
|
+
"-y",
|
|
144
|
+
"--yes",
|
|
145
|
+
is_flag=True,
|
|
146
|
+
help="Skip confirmation prompt",
|
|
147
|
+
)
|
|
148
|
+
@click.pass_obj
|
|
149
|
+
@handle_errors
|
|
150
|
+
def archive(config, archive_all, yes):
|
|
151
|
+
"""Archive existing sessions."""
|
|
152
|
+
provider = validate_and_get_provider(config)
|
|
153
|
+
active_organization_id = config.get("active_organization_id")
|
|
154
|
+
sessions_data = provider.get_sessions(active_organization_id)
|
|
155
|
+
|
|
156
|
+
if not sessions_data or not sessions_data.get("data"):
|
|
157
|
+
click.echo("No sessions found.")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# Filter for active sessions (not archived)
|
|
161
|
+
sessions = [
|
|
162
|
+
s
|
|
163
|
+
for s in sessions_data["data"]
|
|
164
|
+
if s.get("session_status") in ["running", "idle"]
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
if not sessions:
|
|
168
|
+
click.echo("No active sessions found.")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
if archive_all:
|
|
172
|
+
if not yes:
|
|
173
|
+
click.echo("The following sessions will be archived:")
|
|
174
|
+
for sess in sessions:
|
|
175
|
+
title = sess.get("title", "Untitled")
|
|
176
|
+
sess_id = sess.get("id", "N/A")
|
|
177
|
+
click.echo(f" - {title} (ID: {sess_id})")
|
|
178
|
+
if not click.confirm("Are you sure you want to archive all sessions?"):
|
|
179
|
+
click.echo("Operation cancelled.")
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
success_count = 0
|
|
183
|
+
failure_count = 0
|
|
184
|
+
with click.progressbar(
|
|
185
|
+
sessions,
|
|
186
|
+
label="Archiving sessions",
|
|
187
|
+
item_show_func=lambda s: s.get("title", "Untitled") if s else "",
|
|
188
|
+
) as bar:
|
|
189
|
+
for sess in bar:
|
|
190
|
+
try:
|
|
191
|
+
provider.archive_session(active_organization_id, sess.get("id"))
|
|
192
|
+
success_count += 1
|
|
193
|
+
except ProviderError as e:
|
|
194
|
+
failure_count += 1
|
|
195
|
+
title = sess.get("title", "Untitled")
|
|
196
|
+
click.echo(f"\nFailed to archive session '{title}': {str(e)}")
|
|
197
|
+
|
|
198
|
+
click.echo(
|
|
199
|
+
f"\nArchive operation completed. "
|
|
200
|
+
f"Successfully archived: {success_count}, Failed: {failure_count}"
|
|
201
|
+
)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
single_session_archival(sessions, yes, provider, active_organization_id)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def single_session_archival(sessions, yes, provider, organization_id):
|
|
208
|
+
"""Archive a single session selected by the user."""
|
|
209
|
+
click.echo("Available sessions to archive:")
|
|
210
|
+
for idx, sess in enumerate(sessions, 1):
|
|
211
|
+
title = sess.get("title", "Untitled")
|
|
212
|
+
sess_id = sess.get("id", "N/A")
|
|
213
|
+
click.echo(f" {idx}. {title} (ID: {sess_id})")
|
|
214
|
+
|
|
215
|
+
selection = click.prompt("Enter the number of the session to archive", type=int)
|
|
216
|
+
if 1 <= selection <= len(sessions):
|
|
217
|
+
selected_session = sessions[selection - 1]
|
|
218
|
+
title = selected_session.get("title", "Untitled")
|
|
219
|
+
if yes or click.confirm(
|
|
220
|
+
f"Are you sure you want to archive the session '{title}'? "
|
|
221
|
+
f"Archived sessions cannot be modified but can still be viewed."
|
|
222
|
+
):
|
|
223
|
+
try:
|
|
224
|
+
provider.archive_session(organization_id, selected_session.get("id"))
|
|
225
|
+
click.echo(f"Session '{title}' has been archived.")
|
|
226
|
+
except ProviderError as e:
|
|
227
|
+
click.echo(f"Failed to archive session '{title}': {str(e)}")
|
|
228
|
+
else:
|
|
229
|
+
click.echo("Invalid selection. Please try again.")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@session.command()
|
|
233
|
+
@click.argument("title", required=False)
|
|
234
|
+
@click.option(
|
|
235
|
+
"-e",
|
|
236
|
+
"--environment-id",
|
|
237
|
+
help="Environment ID (if not provided, will try to use active environment)",
|
|
238
|
+
)
|
|
239
|
+
@click.option(
|
|
240
|
+
"-m",
|
|
241
|
+
"--model",
|
|
242
|
+
default="claude-sonnet-4-5-20250929",
|
|
243
|
+
help="Model to use (default: claude-sonnet-4-5-20250929)",
|
|
244
|
+
)
|
|
245
|
+
@click.option(
|
|
246
|
+
"-b",
|
|
247
|
+
"--branch",
|
|
248
|
+
help="Branch name to create (auto-generated if not provided)",
|
|
249
|
+
)
|
|
250
|
+
@click.option(
|
|
251
|
+
"--json",
|
|
252
|
+
"json_output",
|
|
253
|
+
is_flag=True,
|
|
254
|
+
help="Output in JSON format",
|
|
255
|
+
)
|
|
256
|
+
@click.pass_obj
|
|
257
|
+
@handle_errors
|
|
258
|
+
def create(config, title, environment_id, model, branch, json_output): # noqa: C901
|
|
259
|
+
"""Create a new Claude Code web session.
|
|
260
|
+
|
|
261
|
+
Provide a title for the session. If no title is provided, you will be prompted.
|
|
262
|
+
If the current directory is a git repository, it will be automatically linked to the session.
|
|
263
|
+
"""
|
|
264
|
+
import json as json_module
|
|
265
|
+
import subprocess
|
|
266
|
+
import re
|
|
267
|
+
|
|
268
|
+
provider = validate_and_get_provider(config)
|
|
269
|
+
active_organization_id = config.get("active_organization_id")
|
|
270
|
+
|
|
271
|
+
# Get the title from user if not provided
|
|
272
|
+
if not title:
|
|
273
|
+
title = click.prompt("Enter the session title")
|
|
274
|
+
|
|
275
|
+
if not title.strip():
|
|
276
|
+
click.echo("Error: Title cannot be empty.")
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Get environment_id from config or parameter
|
|
280
|
+
if not environment_id:
|
|
281
|
+
environment_id = config.get("active_environment_id")
|
|
282
|
+
if not environment_id:
|
|
283
|
+
# Try to get environments and let user select
|
|
284
|
+
try:
|
|
285
|
+
environments_data = provider.get_environments(active_organization_id)
|
|
286
|
+
environments = environments_data.get("environments", [])
|
|
287
|
+
|
|
288
|
+
if not environments:
|
|
289
|
+
click.echo("Error: No environments found.")
|
|
290
|
+
click.echo(
|
|
291
|
+
"Please create an environment first or use -e flag to specify one."
|
|
292
|
+
)
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
# Show available environments
|
|
296
|
+
click.echo("Available environments:")
|
|
297
|
+
for idx, env in enumerate(environments, 1):
|
|
298
|
+
env_id = env.get("environment_id", "N/A")
|
|
299
|
+
name = env.get("name", "Unnamed")
|
|
300
|
+
state = env.get("state", "unknown")
|
|
301
|
+
click.echo(f" {idx}. {name} ({state}) - {env_id}")
|
|
302
|
+
|
|
303
|
+
# Prompt user to select
|
|
304
|
+
selection = click.prompt(
|
|
305
|
+
"Select an environment number", type=int, default=1
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if 1 <= selection <= len(environments):
|
|
309
|
+
environment_id = environments[selection - 1].get("environment_id")
|
|
310
|
+
if not json_output:
|
|
311
|
+
click.echo(
|
|
312
|
+
f"Using environment: {environments[selection - 1].get('name')}"
|
|
313
|
+
)
|
|
314
|
+
else:
|
|
315
|
+
click.echo("Invalid selection.")
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
except ProviderError as e:
|
|
319
|
+
click.echo(f"Error: Could not retrieve environments: {str(e)}")
|
|
320
|
+
click.echo("Please use -e flag to specify an environment ID.")
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
# Try to detect git repository context and verify it's available
|
|
324
|
+
git_repo_url = None
|
|
325
|
+
git_repo_owner = None
|
|
326
|
+
git_repo_name = None
|
|
327
|
+
local_repo_detected = False
|
|
328
|
+
local_owner = None
|
|
329
|
+
local_name = None
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
# Get git remote URL
|
|
333
|
+
result = subprocess.run(
|
|
334
|
+
["git", "remote", "get-url", "origin"],
|
|
335
|
+
capture_output=True,
|
|
336
|
+
text=True,
|
|
337
|
+
check=True,
|
|
338
|
+
)
|
|
339
|
+
git_remote = result.stdout.strip()
|
|
340
|
+
|
|
341
|
+
# Parse GitHub URL (supports both SSH and HTTPS)
|
|
342
|
+
# SSH: git@github.com:owner/repo.git
|
|
343
|
+
# HTTPS: https://github.com/owner/repo.git
|
|
344
|
+
github_ssh_pattern = r"git@github\.com:([^/]+)/(.+?)(?:\.git)?$"
|
|
345
|
+
github_https_pattern = r"https://github\.com/([^/]+)/(.+?)(?:\.git)?$"
|
|
346
|
+
|
|
347
|
+
match = re.match(github_ssh_pattern, git_remote) or re.match(
|
|
348
|
+
github_https_pattern, git_remote
|
|
349
|
+
)
|
|
350
|
+
if match:
|
|
351
|
+
local_owner = match.group(1)
|
|
352
|
+
local_name = match.group(2)
|
|
353
|
+
local_repo_detected = True
|
|
354
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
355
|
+
# Not in a git repository or git not available
|
|
356
|
+
local_repo_detected = False
|
|
357
|
+
|
|
358
|
+
# Get available repos to verify local repo is connected
|
|
359
|
+
repo_available = False
|
|
360
|
+
if local_repo_detected:
|
|
361
|
+
try:
|
|
362
|
+
repos_data = provider.get_code_repos(active_organization_id)
|
|
363
|
+
repos = repos_data.get("repos", [])
|
|
364
|
+
|
|
365
|
+
# Check if local repo is in available repos
|
|
366
|
+
for repo_data in repos:
|
|
367
|
+
repo = repo_data.get("repo", {})
|
|
368
|
+
owner = repo.get("owner", {}).get("login")
|
|
369
|
+
name = repo.get("name")
|
|
370
|
+
if owner == local_owner and name == local_name:
|
|
371
|
+
git_repo_owner = local_owner
|
|
372
|
+
git_repo_name = local_name
|
|
373
|
+
git_repo_url = (
|
|
374
|
+
f"https://github.com/{git_repo_owner}/{git_repo_name}"
|
|
375
|
+
)
|
|
376
|
+
repo_available = True
|
|
377
|
+
if not json_output:
|
|
378
|
+
click.echo(
|
|
379
|
+
f"Using detected repository: {git_repo_owner}/{git_repo_name}"
|
|
380
|
+
)
|
|
381
|
+
break
|
|
382
|
+
|
|
383
|
+
if not repo_available and not json_output:
|
|
384
|
+
click.echo(
|
|
385
|
+
f"\nDetected local repository {local_owner}/{local_name}, but it's not connected to Claude Code."
|
|
386
|
+
)
|
|
387
|
+
click.echo(
|
|
388
|
+
"You need to connect this repository via GitHub OAuth first."
|
|
389
|
+
)
|
|
390
|
+
click.echo("Available repositories:")
|
|
391
|
+
except ProviderError:
|
|
392
|
+
repos = []
|
|
393
|
+
|
|
394
|
+
# If no valid repo, prompt user to select one
|
|
395
|
+
if not repo_available and not json_output:
|
|
396
|
+
try:
|
|
397
|
+
# If we haven't fetched repos yet, fetch them now
|
|
398
|
+
if not local_repo_detected or not repos:
|
|
399
|
+
repos_data = provider.get_code_repos(active_organization_id)
|
|
400
|
+
repos = repos_data.get("repos", [])
|
|
401
|
+
|
|
402
|
+
if repos:
|
|
403
|
+
for idx, repo_data in enumerate(repos, 1):
|
|
404
|
+
repo = repo_data.get("repo", {})
|
|
405
|
+
name = repo.get("name", "Unknown")
|
|
406
|
+
owner = repo.get("owner", {}).get("login", "Unknown")
|
|
407
|
+
click.echo(f" {idx}. {owner}/{name}")
|
|
408
|
+
click.echo(
|
|
409
|
+
f" {len(repos) + 1}. Skip (create session without repository)"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
selection = click.prompt(
|
|
413
|
+
"Select a repository number", type=int, default=len(repos) + 1
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if 1 <= selection <= len(repos):
|
|
417
|
+
selected_repo = repos[selection - 1].get("repo", {})
|
|
418
|
+
git_repo_owner = selected_repo.get("owner", {}).get("login")
|
|
419
|
+
git_repo_name = selected_repo.get("name")
|
|
420
|
+
if git_repo_owner and git_repo_name:
|
|
421
|
+
git_repo_url = (
|
|
422
|
+
f"https://github.com/{git_repo_owner}/{git_repo_name}"
|
|
423
|
+
)
|
|
424
|
+
click.echo(
|
|
425
|
+
f"Using repository: {git_repo_owner}/{git_repo_name}"
|
|
426
|
+
)
|
|
427
|
+
elif selection == len(repos) + 1:
|
|
428
|
+
click.echo("Creating session without git repository context")
|
|
429
|
+
else:
|
|
430
|
+
click.echo(
|
|
431
|
+
"Invalid selection. Creating session without repository."
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
except ProviderError:
|
|
435
|
+
# If we can't get repos, just continue without repo context
|
|
436
|
+
if not json_output:
|
|
437
|
+
click.echo("Creating session without git repository context")
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
result = provider.create_session(
|
|
441
|
+
organization_id=active_organization_id,
|
|
442
|
+
title=title,
|
|
443
|
+
environment_id=environment_id,
|
|
444
|
+
git_repo_url=git_repo_url,
|
|
445
|
+
git_repo_owner=git_repo_owner,
|
|
446
|
+
git_repo_name=git_repo_name,
|
|
447
|
+
branch_name=branch,
|
|
448
|
+
model=model,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
session_id = result.get("id", "N/A")
|
|
452
|
+
session_title = result.get("title", "N/A")
|
|
453
|
+
session_status = result.get("session_status", "N/A")
|
|
454
|
+
|
|
455
|
+
# Extract branch name from outcomes
|
|
456
|
+
branch_info = "N/A"
|
|
457
|
+
try:
|
|
458
|
+
outcomes = result.get("session_context", {}).get("outcomes", [])
|
|
459
|
+
for outcome in outcomes:
|
|
460
|
+
if outcome.get("type") == "git_repository":
|
|
461
|
+
branches = outcome.get("git_info", {}).get("branches", [])
|
|
462
|
+
if branches:
|
|
463
|
+
branch_info = branches[0]
|
|
464
|
+
except (AttributeError, TypeError, KeyError):
|
|
465
|
+
pass
|
|
466
|
+
|
|
467
|
+
if json_output:
|
|
468
|
+
click.echo(json_module.dumps(result, indent=2))
|
|
469
|
+
else:
|
|
470
|
+
click.echo("Session created successfully!")
|
|
471
|
+
click.echo(f"ID: {session_id}")
|
|
472
|
+
click.echo(f"Title: {session_title}")
|
|
473
|
+
click.echo(f"Status: {session_status}")
|
|
474
|
+
if branch_info != "N/A":
|
|
475
|
+
click.echo(f"Branch: {branch_info}")
|
|
476
|
+
|
|
477
|
+
click.echo(f"\nView session at: https://claude.ai/code/{session_id}")
|
|
478
|
+
click.echo(
|
|
479
|
+
"\nNote: Session starts idle. Send a message through the web UI to begin."
|
|
480
|
+
)
|
|
481
|
+
click.echo("\n--- Streaming session events (Ctrl+C to stop) ---\n")
|
|
482
|
+
click.echo("Connecting to event stream...")
|
|
483
|
+
|
|
484
|
+
# Stream session events
|
|
485
|
+
try:
|
|
486
|
+
event_count = 0
|
|
487
|
+
for event in provider.stream_session_events(
|
|
488
|
+
active_organization_id, session_id
|
|
489
|
+
):
|
|
490
|
+
event_count += 1
|
|
491
|
+
|
|
492
|
+
# Debug: show raw events for now
|
|
493
|
+
click.echo(f"[Event {event_count}] {json_module.dumps(event)}")
|
|
494
|
+
|
|
495
|
+
if "error" in event:
|
|
496
|
+
click.echo(f"Error: {event['error']}")
|
|
497
|
+
break
|
|
498
|
+
|
|
499
|
+
# Handle different event types
|
|
500
|
+
event_type = event.get("type")
|
|
501
|
+
if event_type == "message":
|
|
502
|
+
content = event.get("content", "")
|
|
503
|
+
if content:
|
|
504
|
+
click.echo(f"Claude: {content}")
|
|
505
|
+
elif event_type == "session_status":
|
|
506
|
+
status = event.get("status", "")
|
|
507
|
+
if status:
|
|
508
|
+
click.echo(f"Status: {status}")
|
|
509
|
+
|
|
510
|
+
if event_count == 0:
|
|
511
|
+
click.echo("\nNo events received from session.")
|
|
512
|
+
click.echo("The session may still be initializing.")
|
|
513
|
+
except KeyboardInterrupt:
|
|
514
|
+
click.echo("\n\nSession streaming stopped by user.")
|
|
515
|
+
click.echo(f"Session {session_id} continues running in the background.")
|
|
516
|
+
click.echo(
|
|
517
|
+
"You can view it at: https://claude.ai/code/session_{session_id}"
|
|
518
|
+
)
|
|
519
|
+
except Exception as e:
|
|
520
|
+
click.echo(f"\nError streaming events: {str(e)}")
|
|
521
|
+
click.echo(
|
|
522
|
+
f"Session {session_id} is still running. View at: https://claude.ai/code/{session_id}"
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
except ProviderError as e:
|
|
526
|
+
click.echo(f"Failed to create session: {str(e)}")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@session.command()
|
|
530
|
+
@click.option(
|
|
531
|
+
"-a",
|
|
532
|
+
"--all",
|
|
533
|
+
"show_all",
|
|
534
|
+
is_flag=True,
|
|
535
|
+
help="Show all sessions including archived (default shows only running and idle)",
|
|
536
|
+
)
|
|
537
|
+
@click.option(
|
|
538
|
+
"--json",
|
|
539
|
+
"json_output",
|
|
540
|
+
is_flag=True,
|
|
541
|
+
help="Output in JSON format",
|
|
542
|
+
)
|
|
543
|
+
@click.pass_obj
|
|
544
|
+
@handle_errors
|
|
545
|
+
def ls(config, show_all, json_output): # noqa: C901
|
|
546
|
+
"""List all web sessions."""
|
|
547
|
+
import json as json_module
|
|
548
|
+
|
|
549
|
+
provider = validate_and_get_provider(config)
|
|
550
|
+
active_organization_id = config.get("active_organization_id")
|
|
551
|
+
sessions_data = provider.get_sessions(active_organization_id)
|
|
552
|
+
|
|
553
|
+
if not sessions_data or not sessions_data.get("data"):
|
|
554
|
+
click.echo("No sessions found.")
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
sessions = sessions_data["data"]
|
|
558
|
+
|
|
559
|
+
# Filter sessions if not showing all
|
|
560
|
+
if not show_all:
|
|
561
|
+
sessions = [
|
|
562
|
+
s for s in sessions if s.get("session_status") in ["running", "idle"]
|
|
563
|
+
]
|
|
564
|
+
|
|
565
|
+
if not sessions:
|
|
566
|
+
click.echo("No active sessions found. Use --all to show archived sessions.")
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
if json_output:
|
|
570
|
+
click.echo(json_module.dumps(sessions, indent=2))
|
|
571
|
+
return
|
|
572
|
+
|
|
573
|
+
# Display sessions in a formatted way
|
|
574
|
+
click.echo(f"Found {len(sessions)} session(s):")
|
|
575
|
+
|
|
576
|
+
for idx, sess in enumerate(sessions, 1):
|
|
577
|
+
session_id = sess.get("id", "N/A")
|
|
578
|
+
title = sess.get("title", "Untitled")
|
|
579
|
+
status = sess.get("session_status", "unknown")
|
|
580
|
+
created_at = sess.get("created_at", "N/A")
|
|
581
|
+
updated_at = sess.get("updated_at", "N/A")
|
|
582
|
+
|
|
583
|
+
# Parse and format timestamps
|
|
584
|
+
try:
|
|
585
|
+
created_dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
|
586
|
+
created_str = created_dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
587
|
+
except (ValueError, AttributeError):
|
|
588
|
+
created_str = created_at
|
|
589
|
+
|
|
590
|
+
try:
|
|
591
|
+
updated_dt = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
|
|
592
|
+
updated_str = updated_dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
593
|
+
except (ValueError, AttributeError):
|
|
594
|
+
updated_str = updated_at
|
|
595
|
+
|
|
596
|
+
# Get repository info if available
|
|
597
|
+
repo_info = ""
|
|
598
|
+
try:
|
|
599
|
+
context = sess.get("session_context", {})
|
|
600
|
+
outcomes = context.get("outcomes", [])
|
|
601
|
+
for outcome in outcomes:
|
|
602
|
+
if outcome.get("type") == "git_repository":
|
|
603
|
+
git_info = outcome.get("git_info", {})
|
|
604
|
+
repo = git_info.get("repo", "")
|
|
605
|
+
branches = git_info.get("branches", [])
|
|
606
|
+
if repo:
|
|
607
|
+
repo_info = f"\n Repository: {repo}"
|
|
608
|
+
if branches:
|
|
609
|
+
repo_info += f"\n Branch: {branches[0]}"
|
|
610
|
+
except (AttributeError, TypeError, KeyError):
|
|
611
|
+
# Skip repo info if structure is unexpected
|
|
612
|
+
pass
|
|
613
|
+
|
|
614
|
+
# Status text
|
|
615
|
+
status_text = status.capitalize()
|
|
616
|
+
|
|
617
|
+
click.echo(f"\n{idx}. {title}")
|
|
618
|
+
click.echo(f" ID: {session_id}")
|
|
619
|
+
click.echo(f" Status: {status_text}")
|
|
620
|
+
click.echo(f" Created: {created_str}")
|
|
621
|
+
click.echo(f" Updated: {updated_str}")
|
|
622
|
+
if repo_info:
|
|
623
|
+
click.echo(repo_info)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
__all__ = ["session"]
|