cc-sessions 1.2.0 → 1.4.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 (6) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +2 -0
  4. data/bin/cc +59 -3
  5. data/bin/cc-bookmark +101 -9
  6. metadata +4 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a71cdec4c4feb8c680d3c29c2871393cd7af2255c3960f5a2b36eabc64fa9a7d
4
- data.tar.gz: 03a816b0de6e0dd58017f816dc31e1c2b4acc7b77c159191dfa5f6e91faf6b6b
3
+ metadata.gz: 7c743c60e405530074e227bd6f4b02165cd78c217f0485898bf91505300db5eb
4
+ data.tar.gz: 5137132931a0bba947285cf71c36e64b978495ef6235128d56e6841702ca093b
5
5
  SHA512:
6
- metadata.gz: d9fc6662868b728dff1a09b8ae9962f7efed1f32166048758366a2fb72dbda40de0a432b02817a4b2a3c7e854df3e089a272abbc1da288276cf8f85c4213376f
7
- data.tar.gz: 84073e656ccf0dad4aa8fd7e01ffc6768d5863dd12cda83475ec4332dbb2cd10a7f736ff9481399abc77289480a866a23847f11721663d3e24fc7f4c8abf0a1f
6
+ metadata.gz: 1f1b848071bce7431e0f2de516e5ab314397be2a0df0c4447c744e4489032e9b509bc540bc5a69fdae170d2a619dfce8eeb9292f33aaf63cc6624e719afe2275
7
+ data.tar.gz: 771350b8e375aa0a6399dcce5f17f335b87d612e65d346eb5a5db1e19b65114e30f39be5edb3bdff1303d743e7dd04bcd2ebcf0fda1b7df5233c47d72ebcf7e8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0] - 2026-03-03
4
+
5
+ ### Fixed
6
+ - Resuming a session that had a context continuation now auto-follows to the latest session ID instead of jumping back to the old one
7
+ - Re-bookmarking (`/bm`) in a continued session now uses the detected (newest) session ID instead of the stale env var
8
+
9
+ ### Changed
10
+ - `resume_session` detects newer `.jsonl` files and auto-migrates the bookmark before resuming
11
+ - `cc-bookmark` prefers detected session ID over `CC_SESSION_ID` env var in bookmark mode
12
+ - Old bookmarks and breadcrumbs cleaned up automatically during migration
13
+
14
+ ## [1.3.0] - 2026-02-27
15
+
16
+ ### Added
17
+ - Running session indicator (green `●`) in `cc -l` interactive list
18
+ - Resume breadcrumbs to track session continuations across context resets
19
+ - `CC_SESSION_ID` and `CC_RESUME_TAGS` env vars passed to resumed sessions
20
+ - `exclude_id` support in session detection for sibling session discovery
21
+
22
+ ### Changed
23
+ - `cc-bookmark` now checks env vars first for reliable session identification
24
+ - `/bm?` auto-migrates bookmark to new session ID after context reset
25
+
3
26
  ## [1.2.0] - 2026-02-19
4
27
 
5
28
  ### Added
data/README.md CHANGED
@@ -18,6 +18,8 @@ meaningful names and quickly resume them.
18
18
  - **Check bookmark** with `/bm?` to see current tags
19
19
  - **Resume sessions** with `cc tag` from anywhere
20
20
  - **Interactive list** with `cc -l` (arrow keys or j/k to navigate)
21
+ - **Running indicator** shows green `●` next to active sessions in `cc -l`
22
+ - **Auto-follow continuations** — when Claude creates a new session (context reset), `cc` detects it and resumes the latest session automatically
21
23
  - **Delete bookmarks** with `d` in list or `cc -d tag`
22
24
  - **Auto-install** the `/bm` command and permission on first run
23
25
  - **Zero dependencies** - pure Ruby
data/bin/cc CHANGED
@@ -17,6 +17,7 @@
17
17
  require 'json'
18
18
  require 'fileutils'
19
19
  require 'io/console'
20
+ require 'set'
20
21
 
21
22
  # Simple ANSI helpers
22
23
  def cyan(s) = "\e[36m#{s}\e[0m"
@@ -186,6 +187,20 @@ def delete_bookmark_by_tag(tag)
186
187
  end
187
188
  end
188
189
 
190
+ def running_session_dirs
191
+ pids = `pgrep -x claude 2>/dev/null`.split.map(&:to_i)
192
+ dirs = Set.new
193
+ pids.each do |pid|
194
+ cwd_link = "/proc/#{pid}/cwd"
195
+ next unless File.exist?(cwd_link)
196
+ begin
197
+ dirs << resolve_session_dir(File.readlink(cwd_link))
198
+ rescue Errno::EACCES
199
+ end
200
+ end
201
+ dirs
202
+ end
203
+
189
204
  def list_bookmarks
190
205
  bookmarks = load_bookmarks
191
206
 
@@ -196,8 +211,11 @@ def list_bookmarks
196
211
  return
197
212
  end
198
213
 
214
+ running = running_session_dirs
215
+
199
216
  items = bookmarks['sessions'].map do |id, entry|
200
- { id: id, path: entry['path'], tags: entry['tags'], exists: Dir.exist?(entry['path']) }
217
+ { id: id, path: entry['path'], tags: entry['tags'], exists: Dir.exist?(entry['path']),
218
+ running: running.include?(entry['path']) }
201
219
  end
202
220
 
203
221
  index = 0
@@ -211,8 +229,14 @@ def list_bookmarks
211
229
  print clear_line
212
230
  tag_str = cyan(item[:tags].join(', '))
213
231
  path_str = dim(item[:path])
214
- missing = item[:exists] ? '' : red(' [NOT FOUND]')
215
- line = "#{tag_str} \u2192 #{path_str}#{missing}"
232
+ status = if !item[:exists]
233
+ red(' [NOT FOUND]')
234
+ elsif item[:running]
235
+ green(' ●')
236
+ else
237
+ ''
238
+ end
239
+ line = "#{tag_str} \u2192 #{path_str}#{status}"
216
240
 
217
241
  if i == index
218
242
  puts "\u25b8 #{line}"
@@ -366,11 +390,43 @@ def resume_session(session_id, path)
366
390
  exit 1
367
391
  end
368
392
 
393
+ bookmarks = load_bookmarks
394
+ entry = bookmarks['sessions'][session_id]
395
+
396
+ # Auto-migrate: if a newer session exists (context continuation), use it
397
+ unless session_id.start_with?('path:')
398
+ session_dir = resolve_session_dir(path)
399
+ newest_id = detect_session_id(session_dir)
400
+ if newest_id && newest_id != session_id && entry
401
+ puts dim("Session was continued — following to latest.")
402
+ bookmarks['sessions'].delete(session_id)
403
+ bookmarks['sessions'][newest_id] = entry
404
+ save_bookmarks(bookmarks)
405
+ # Clean up old breadcrumb if any
406
+ FileUtils.rm_f(File.join(CONFIG_DIR, 'resumed', "#{session_id}.json"))
407
+ session_id = newest_id
408
+ end
409
+ end
410
+
411
+ # Save resume breadcrumb so cc-bookmark can track session continuations
412
+ # (when context resets, Claude creates a new session ID)
413
+ if entry && !session_id.start_with?('path:')
414
+ resume_dir = File.join(CONFIG_DIR, 'resumed')
415
+ FileUtils.mkdir_p(resume_dir)
416
+ File.write(File.join(resume_dir, "#{session_id}.json"), entry['tags'].to_json)
417
+ end
418
+
369
419
  Dir.chdir(path) do
370
420
  # Emit OSC 7 so wezterm knows the new cwd (for Alt+n, etc.)
371
421
  host = `hostname`.strip
372
422
  print "\033]7;file://#{host}#{path}\007"
373
423
  puts "Resuming session in: #{path}"
424
+
425
+ # Export session info so cc-bookmark/hooks can reliably identify this session
426
+ ENV['CC_SESSION_ID'] = session_id
427
+ tags = entry ? entry['tags'].join(',') : ''
428
+ ENV['CC_RESUME_TAGS'] = tags unless tags.empty?
429
+
374
430
  if session_id.start_with?('path:')
375
431
  exec('claude', '-c')
376
432
  else
data/bin/cc-bookmark CHANGED
@@ -10,6 +10,7 @@ require 'fileutils'
10
10
  CONFIG_DIR = File.expand_path('~/.cc-sessions')
11
11
  BOOKMARKS_FILE = File.join(CONFIG_DIR, 'bookmarks.json')
12
12
  CLAUDE_PROJECTS = File.expand_path('~/.claude/projects')
13
+ RESUME_DIR = File.join(CONFIG_DIR, 'resumed')
13
14
 
14
15
  def resolve_session_dir(cwd)
15
16
  path = cwd
@@ -23,19 +24,52 @@ def resolve_session_dir(cwd)
23
24
  cwd
24
25
  end
25
26
 
26
- def detect_session_id(session_dir)
27
+ def detect_session_id(session_dir, exclude_id: nil)
27
28
  encoded = session_dir.gsub('/', '-')
28
29
  project_dir = File.join(CLAUDE_PROJECTS, encoded)
29
30
  return nil unless Dir.exist?(project_dir)
30
31
 
31
- newest = Dir.glob(File.join(project_dir, '*.jsonl'))
32
- .select { |f| File.file?(f) }
33
- .max_by { |f| File.mtime(f) }
32
+ candidates = Dir.glob(File.join(project_dir, '*.jsonl'))
33
+ .select { |f| File.file?(f) }
34
+ candidates.reject! { |f| File.basename(f, '.jsonl') == exclude_id } if exclude_id
35
+
36
+ newest = candidates.max_by { |f| File.mtime(f) }
34
37
  return nil unless newest
35
38
 
36
39
  File.basename(newest, '.jsonl')
37
40
  end
38
41
 
42
+ # Find all session IDs in the same project directory
43
+ def sibling_session_ids(session_dir)
44
+ encoded = session_dir.gsub('/', '-')
45
+ project_dir = File.join(CLAUDE_PROJECTS, encoded)
46
+ return [] unless Dir.exist?(project_dir)
47
+
48
+ Dir.glob(File.join(project_dir, '*.jsonl'))
49
+ .select { |f| File.file?(f) }
50
+ .map { |f| File.basename(f, '.jsonl') }
51
+ end
52
+
53
+ # Check resume breadcrumbs left by `cc` to find tags for a continued session.
54
+ # When `cc <tag>` resumes a session that later gets a new ID (context reset),
55
+ # the breadcrumb links the old session ID to its tags.
56
+ def find_resume_breadcrumb(session_dir, current_id, bookmarks)
57
+ return nil unless Dir.exist?(RESUME_DIR)
58
+
59
+ siblings = sibling_session_ids(session_dir)
60
+ siblings.each do |sid|
61
+ next if sid == current_id
62
+ breadcrumb = File.join(RESUME_DIR, "#{sid}.json")
63
+ next unless File.exist?(breadcrumb)
64
+
65
+ tags = JSON.parse(File.read(breadcrumb))
66
+ return [sid, tags] if tags.is_a?(Array)
67
+ end
68
+ nil
69
+ rescue
70
+ nil
71
+ end
72
+
39
73
  def load_bookmarks
40
74
  return { 'version' => 2, 'sessions' => {} } unless File.exist?(BOOKMARKS_FILE)
41
75
 
@@ -52,7 +86,6 @@ end
52
86
  def migrate_bookmarks(old_data)
53
87
  sessions = {}
54
88
  old_data.each do |path, tags|
55
- # Try to find a real session ID for this path
56
89
  sid = detect_session_id(path)
57
90
  key = sid || "path:#{path}"
58
91
  sessions[key] = { 'path' => path, 'tags' => tags }
@@ -71,23 +104,82 @@ FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
71
104
 
72
105
  bookmarks = load_bookmarks
73
106
  cwd = resolve_session_dir(Dir.pwd)
74
- session_id = detect_session_id(cwd)
107
+ original_id = ENV['CC_SESSION_ID'] # Set by `cc` wrapper — reliable
108
+ session_id = detect_session_id(cwd) # Fallback — picks newest .jsonl
75
109
  tags = ARGV
76
110
 
77
111
  if tags.empty?
78
112
  # Query mode: show tags for current session
79
- if session_id
113
+ # Priority: check the known original session ID first (from env var)
114
+ if original_id
115
+ entry = bookmarks['sessions'][original_id]
116
+ if entry
117
+ # Session ID hasn't changed, or bookmark is still on the original ID
118
+ puts "Current bookmark: #{entry['tags'].join(', ')}"
119
+ else
120
+ # Session ID changed (compaction/context reset).
121
+ # The bookmark is on the original ID but CC created a new session.
122
+ # Check if we have tags from env var or breadcrumb.
123
+ resume_tags = ENV['CC_RESUME_TAGS']&.split(',')
124
+ breadcrumb_file = File.join(RESUME_DIR, "#{original_id}.json")
125
+ if resume_tags.nil? && File.exist?(breadcrumb_file)
126
+ resume_tags = JSON.parse(File.read(breadcrumb_file)) rescue nil
127
+ end
128
+
129
+ if resume_tags
130
+ # Find the NEW session ID (newest .jsonl excluding the original)
131
+ new_id = detect_session_id(cwd, exclude_id: original_id)
132
+ if new_id
133
+ # Don't overwrite if the target session already has a bookmark
134
+ existing = bookmarks['sessions'][new_id]
135
+ if existing
136
+ puts "Current bookmark: #{existing['tags'].join(', ')}"
137
+ else
138
+ bookmarks['sessions'][new_id] = { 'path' => cwd, 'tags' => resume_tags }
139
+ save_bookmarks(bookmarks)
140
+ FileUtils.rm_f(breadcrumb_file)
141
+ puts "Current bookmark: #{resume_tags.join(', ')}"
142
+ end
143
+ else
144
+ puts "No bookmark for this session. Usage: /bm tag1 tag2 ..."
145
+ end
146
+ else
147
+ puts "No bookmark for this session. Usage: /bm tag1 tag2 ..."
148
+ end
149
+ end
150
+ elsif session_id
80
151
  entry = bookmarks['sessions'][session_id]
81
152
  if entry
82
153
  puts "Current bookmark: #{entry['tags'].join(', ')}"
83
154
  else
84
- puts "No bookmark for this session. Usage: /bm tag1 tag2 ..."
155
+ # Fallback: check resume breadcrumbs (legacy behavior, no env var)
156
+ match = find_resume_breadcrumb(cwd, session_id, bookmarks)
157
+ if match
158
+ old_id, old_tags = match
159
+ bookmarks['sessions'].delete(old_id)
160
+ bookmarks['sessions'][session_id] = { 'path' => cwd, 'tags' => old_tags }
161
+ save_bookmarks(bookmarks)
162
+ FileUtils.rm_f(File.join(RESUME_DIR, "#{old_id}.json"))
163
+ puts "Current bookmark: #{old_tags.join(', ')}"
164
+ else
165
+ puts "No bookmark for this session. Usage: /bm tag1 tag2 ..."
166
+ end
85
167
  end
86
168
  else
87
169
  puts "Could not detect session ID. Usage: /bm tag1 tag2 ..."
88
170
  end
89
171
  else
90
- key = session_id || "path:#{cwd}"
172
+ # Bookmark mode: set tags for current session
173
+ # Prefer detected (newest) session ID over env var — the env var may be stale
174
+ # after a context continuation where Claude created a new session ID
175
+ key = session_id || original_id || "path:#{cwd}"
176
+
177
+ # Clean up old bookmark if session ID changed (migration)
178
+ if original_id && original_id != key && bookmarks['sessions'][original_id]
179
+ bookmarks['sessions'].delete(original_id)
180
+ FileUtils.rm_f(File.join(RESUME_DIR, "#{original_id}.json"))
181
+ end
182
+
91
183
  bookmarks['sessions'][key] = { 'path' => cwd, 'tags' => tags }
92
184
  save_bookmarks(bookmarks)
93
185
  puts "Bookmarked: #{cwd}"
metadata CHANGED
@@ -1,19 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cc-sessions
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-19 00:00:00.000000000 Z
11
+ date: 2026-03-03 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'A simple tool for bookmarking and resuming Claude Code sessions. Tag
14
14
  sessions with meaningful names using /bm inside Claude Code, then quickly resume
15
- them with ''cc <tag>'' from anywhere. UPDATE v1.2.0: Session ID-based bookmarks,
16
- auto-migration, OSC 7 for wezterm CWD tracking.'
15
+ them with ''cc <tag>'' from anywhere. v1.4.0: Auto-follows context continuations
16
+ to latest session on resume.'
17
17
  email:
18
18
  - g@isene.com
19
19
  executables: