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.
Files changed (28) hide show
  1. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/PKG-INFO +1 -1
  2. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.egg-info/PKG-INFO +1 -1
  3. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions_gui.py +90 -2
  4. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/pyproject.toml +1 -1
  5. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/LICENSE +0 -0
  6. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/README.md +0 -0
  7. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.egg-info/SOURCES.txt +0 -0
  8. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.egg-info/dependency_links.txt +0 -0
  9. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.egg-info/entry_points.txt +0 -0
  10. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.egg-info/top_level.txt +0 -0
  11. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/claude_code_sessions.py +0 -0
  12. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/setup.cfg +0 -0
  13. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_cli.py +0 -0
  14. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_commands.py +0 -0
  15. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_containment.py +0 -0
  16. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_discovery.py +0 -0
  17. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_encoding.py +0 -0
  18. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_engine.py +0 -0
  19. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_golden.py +0 -0
  20. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_helpers.py +0 -0
  21. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_journal.py +0 -0
  22. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_plan_move.py +0 -0
  23. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_recover.py +0 -0
  24. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_rows.py +0 -0
  25. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_scaffold.py +0 -0
  26. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_sync.py +0 -0
  27. {claude_code_sessions-0.9.2 → claude_code_sessions-0.9.3}/tests/test_transcripts.py +0 -0
  28. {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.2
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-sessions
3
- Version: 0.9.2
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.2"
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"