claude-code-sessions 0.9.2__tar.gz → 0.9.3__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.
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/PKG-INFO +1 -1
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.egg-info/PKG-INFO +1 -1
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions_gui.py +90 -2
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/pyproject.toml +1 -1
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/LICENSE +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/README.md +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.egg-info/SOURCES.txt +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.egg-info/dependency_links.txt +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.egg-info/entry_points.txt +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.egg-info/top_level.txt +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/setup.cfg +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_cli.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_commands.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_containment.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_discovery.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_encoding.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_engine.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_golden.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_helpers.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_journal.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_plan_move.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_recover.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_rows.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_scaffold.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_sync.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_transcripts.py +0 -0
- {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_undo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-sessions
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
4
4
|
Summary: Unofficial CLI for Claude Code sessions on disk: make a session visible under your other Claude account on the same machine, or move one between project folders. Journaled, reversible, fails closed.
|
|
5
5
|
Author: Craig Stoller
|
|
6
6
|
License: MIT
|
{claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-sessions
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.3
|
|
4
4
|
Summary: Unofficial CLI for Claude Code sessions on disk: make a session visible under your other Claude account on the same machine, or move one between project folders. Journaled, reversible, fails closed.
|
|
5
5
|
Author: Craig Stoller
|
|
6
6
|
License: MIT
|
|
@@ -77,6 +77,10 @@ class SyncApp:
|
|
|
77
77
|
self.env = ccs.default_env()
|
|
78
78
|
self.manifest = None
|
|
79
79
|
self.dest_choice = load_pref()
|
|
80
|
+
# Never persisted, unlike dest_choice: the destination is a stable fact
|
|
81
|
+
# about this machine, while "which account is signed in" changes every
|
|
82
|
+
# time you switch. A remembered answer would be a stale assertion.
|
|
83
|
+
self.live_choice = ""
|
|
80
84
|
root.title("Claude session sync")
|
|
81
85
|
root.geometry("880x580")
|
|
82
86
|
root.minsize(700, 460)
|
|
@@ -128,7 +132,15 @@ class SyncApp:
|
|
|
128
132
|
self.refresh_btn.configure(state=state)
|
|
129
133
|
|
|
130
134
|
# ---------------------------------------------------------------- planning
|
|
131
|
-
def refresh(self):
|
|
135
|
+
def refresh(self, keep_live=False):
|
|
136
|
+
# An explicit Refresh always re-asks which account is live. A --live
|
|
137
|
+
# assertion is a statement about RIGHT NOW, not a setting: keeping it
|
|
138
|
+
# across a deliberate re-look would let a stale answer survive an
|
|
139
|
+
# account switch, which is the very failure --live exists to prevent.
|
|
140
|
+
# (Only the picker re-plans with keep_live=True, immediately after
|
|
141
|
+
# being told.)
|
|
142
|
+
if not keep_live:
|
|
143
|
+
self.live_choice = ""
|
|
132
144
|
self.busy(True)
|
|
133
145
|
self.apply_btn.configure(state="disabled")
|
|
134
146
|
self.status.set("Planning...")
|
|
@@ -138,7 +150,7 @@ class SyncApp:
|
|
|
138
150
|
|
|
139
151
|
def _plan_worker(self):
|
|
140
152
|
try:
|
|
141
|
-
flags = ccs.SyncFlags(to=self.dest_choice)
|
|
153
|
+
flags = ccs.SyncFlags(to=self.dest_choice, live=self.live_choice)
|
|
142
154
|
manifest = ccs.plan_sync(self.env, flags)
|
|
143
155
|
self.root.after(0, self._plan_done, manifest, None)
|
|
144
156
|
except ccs.Refusal as exc:
|
|
@@ -151,6 +163,15 @@ class SyncApp:
|
|
|
151
163
|
if problem:
|
|
152
164
|
kind, msg = problem
|
|
153
165
|
self.manifest = None
|
|
166
|
+
if (kind == "refusal" and not self.live_choice
|
|
167
|
+
and "cannot identify the signed-in account" in msg
|
|
168
|
+
and "disagree" in msg):
|
|
169
|
+
self.status.set("Which account is Claude Desktop signed into?")
|
|
170
|
+
self.detail.set("The two files that record this disagree, and either can "
|
|
171
|
+
"be the stale one - so the tool refuses to guess.")
|
|
172
|
+
self.show([msg])
|
|
173
|
+
self._offer_live_picker()
|
|
174
|
+
return
|
|
154
175
|
if kind == "refusal" and "more than one other account store" in msg:
|
|
155
176
|
self.status.set("Which account should these sessions go to?")
|
|
156
177
|
self.detail.set("More than one other account store exists on this "
|
|
@@ -255,6 +276,72 @@ class SyncApp:
|
|
|
255
276
|
ttk.Button(win, text=line, command=pick).pack(fill="x", padx=PAD, pady=2)
|
|
256
277
|
ttk.Button(win, text="Cancel", command=win.destroy).pack(pady=PAD)
|
|
257
278
|
|
|
279
|
+
def _account_label(self, uuid):
|
|
280
|
+
"""email (id) when the email can be recovered, else just the id."""
|
|
281
|
+
email = ""
|
|
282
|
+
try:
|
|
283
|
+
with open(os.path.join(self.env.home, ".claude.json"),
|
|
284
|
+
encoding="utf-8") as fh:
|
|
285
|
+
import json
|
|
286
|
+
oa = (json.load(fh) or {}).get("oauthAccount") or {}
|
|
287
|
+
if isinstance(oa, dict) and oa.get("accountUuid") == uuid:
|
|
288
|
+
email = oa.get("emailAddress") or ""
|
|
289
|
+
except (OSError, ValueError, AttributeError, TypeError):
|
|
290
|
+
pass
|
|
291
|
+
email = email or ccs.dormant_account_email(self.env, uuid) or ""
|
|
292
|
+
return ("{0} ({1}…)".format(email, uuid[:8]) if email
|
|
293
|
+
else "{0}…".format(uuid[:8]))
|
|
294
|
+
|
|
295
|
+
def _offer_live_picker(self):
|
|
296
|
+
"""Turn the identity-disagreement refusal into an assertion, per RULING 5.
|
|
297
|
+
|
|
298
|
+
Deliberately NOT a "just proceed" button. The user is stating a fact -
|
|
299
|
+
which account the desktop app is signed into - so both candidates are
|
|
300
|
+
shown neutrally, neither is pre-selected, and the consequence is spelled
|
|
301
|
+
out: the OTHER store is the one that gets written.
|
|
302
|
+
"""
|
|
303
|
+
dis = ccs._identity_disagreement(self.env)
|
|
304
|
+
if not dis:
|
|
305
|
+
return # shape changed: leave the raw refusal
|
|
306
|
+
# One button per STORE, not per account, and the asserted value is the
|
|
307
|
+
# store's path. An account can own several org directories - this very
|
|
308
|
+
# machine has two per account - and a bare account uuid then matches
|
|
309
|
+
# more than one store, which _resolve_live_assertion refuses. The user
|
|
310
|
+
# would have been stuck: live_choice is set, so this picker would not
|
|
311
|
+
# reopen, and there is no other way in the window to name an org.
|
|
312
|
+
stores = [(a, o, p) for a, o, p in ccs._account_dirs(self.env) if a in dis]
|
|
313
|
+
if not stores:
|
|
314
|
+
return
|
|
315
|
+
win = tk.Toplevel(self.root)
|
|
316
|
+
win.title("Which account is signed in?")
|
|
317
|
+
win.transient(self.root)
|
|
318
|
+
ttk.Label(win, padding=PAD, justify="left", wraplength=560,
|
|
319
|
+
text="Claude Desktop and the Claude Code CLI disagree about which "
|
|
320
|
+
"account is signed in, and either record can be the stale one.\n\n"
|
|
321
|
+
"Tell it which store the DESKTOP APP is signed into right now. "
|
|
322
|
+
"The OTHER one is what gets written, so an answer that is wrong "
|
|
323
|
+
"writes the store you are actually using.\n\nThis is not "
|
|
324
|
+
"remembered - it is asked again every time.").pack(anchor="w")
|
|
325
|
+
for a, o, p in stores:
|
|
326
|
+
rows = ccs._listing_row_count(p)
|
|
327
|
+
count = ("{0} rows".format(rows) if rows
|
|
328
|
+
else "no listing rows" if rows == 0 else "row count unreadable")
|
|
329
|
+
|
|
330
|
+
def pick(path=p):
|
|
331
|
+
self.live_choice = path # a full path matches exactly one store
|
|
332
|
+
win.destroy()
|
|
333
|
+
self.refresh(keep_live=True)
|
|
334
|
+
ttk.Button(win, command=pick,
|
|
335
|
+
text="Signed in as {0} org {1}… ({2})".format(
|
|
336
|
+
self._account_label(a), o[:8], count)).pack(
|
|
337
|
+
fill="x", padx=PAD, pady=3)
|
|
338
|
+
ttk.Label(win, padding=(PAD, 4), foreground="#555", wraplength=520,
|
|
339
|
+
justify="left",
|
|
340
|
+
text="Or cancel and fix it at the source: run 'claude' then /login as "
|
|
341
|
+
"the account you are using, or switch the desktop app, so the two "
|
|
342
|
+
"records agree.").pack(anchor="w")
|
|
343
|
+
ttk.Button(win, text="Cancel", command=win.destroy).pack(pady=PAD)
|
|
344
|
+
|
|
258
345
|
def forget_destination(self):
|
|
259
346
|
self.dest_choice = ""
|
|
260
347
|
save_pref("")
|
|
@@ -299,6 +386,7 @@ class SyncApp:
|
|
|
299
386
|
self.apply_btn.configure(state="disabled")
|
|
300
387
|
return
|
|
301
388
|
written = sum(1 for r in (self.manifest.get("rows") or []) if r.get("written"))
|
|
389
|
+
self.live_choice = "" # an assertion covers one run, not a session
|
|
302
390
|
self.status.set("Copied {0} session{1}".format(
|
|
303
391
|
written, "" if written == 1 else "s"))
|
|
304
392
|
self.detail.set("Sign into the other account (or restart the app) to see them. "
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "claude-code-sessions"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.3"
|
|
8
8
|
description = "Unofficial CLI for Claude Code sessions on disk: make a session visible under your other Claude account on the same machine, or move one between project folders. Journaled, reversible, fails closed."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
{claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|