cc-sessions 1.2.0 → 1.3.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 +12 -0
  3. data/README.md +1 -0
  4. data/bin/cc +43 -3
  5. data/bin/cc-bookmark +93 -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: 605f4de2854d8c09e7930c20cb0bce3f3b511d6dc4d7f7e87258d1eb9082108f
4
+ data.tar.gz: c87c3a2b615a1631c5932154055b49d8bebec23663031dd1ffb446fe79477cef
5
5
  SHA512:
6
- metadata.gz: d9fc6662868b728dff1a09b8ae9962f7efed1f32166048758366a2fb72dbda40de0a432b02817a4b2a3c7e854df3e089a272abbc1da288276cf8f85c4213376f
7
- data.tar.gz: 84073e656ccf0dad4aa8fd7e01ffc6768d5863dd12cda83475ec4332dbb2cd10a7f736ff9481399abc77289480a866a23847f11721663d3e24fc7f4c8abf0a1f
6
+ metadata.gz: 1f9d6e2c15ff339679c7a65fe43354b2ee4518c8ebe78af68092492244c35e82f4afeac6a119e36914ba63e4734283e188b28b7d65e3fa77cebb3f51e5b056f6
7
+ data.tar.gz: 776a04d367ad4c6e169ebc58819a1cf62baa835f32f9fd9bd680a16d04fbabe65f5fe07b70f251d7221421be944a5e8226b489cd2099f625926243a1505bcba0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.0] - 2026-02-27
4
+
5
+ ### Added
6
+ - Running session indicator (green `●`) in `cc -l` interactive list
7
+ - Resume breadcrumbs to track session continuations across context resets
8
+ - `CC_SESSION_ID` and `CC_RESUME_TAGS` env vars passed to resumed sessions
9
+ - `exclude_id` support in session detection for sibling session discovery
10
+
11
+ ### Changed
12
+ - `cc-bookmark` now checks env vars first for reliable session identification
13
+ - `/bm?` auto-migrates bookmark to new session ID after context reset
14
+
3
15
  ## [1.2.0] - 2026-02-19
4
16
 
5
17
  ### Added
data/README.md CHANGED
@@ -18,6 +18,7 @@ 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`
21
22
  - **Delete bookmarks** with `d` in list or `cc -d tag`
22
23
  - **Auto-install** the `/bm` command and permission on first run
23
24
  - **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,27 @@ def resume_session(session_id, path)
366
390
  exit 1
367
391
  end
368
392
 
393
+ # Save resume breadcrumb so cc-bookmark can track session continuations
394
+ # (when context resets, Claude creates a new session ID)
395
+ bookmarks = load_bookmarks
396
+ entry = bookmarks['sessions'][session_id]
397
+ if entry && !session_id.start_with?('path:')
398
+ resume_dir = File.join(CONFIG_DIR, 'resumed')
399
+ FileUtils.mkdir_p(resume_dir)
400
+ File.write(File.join(resume_dir, "#{session_id}.json"), entry['tags'].to_json)
401
+ end
402
+
369
403
  Dir.chdir(path) do
370
404
  # Emit OSC 7 so wezterm knows the new cwd (for Alt+n, etc.)
371
405
  host = `hostname`.strip
372
406
  print "\033]7;file://#{host}#{path}\007"
373
407
  puts "Resuming session in: #{path}"
408
+
409
+ # Export session info so cc-bookmark/hooks can reliably identify this session
410
+ ENV['CC_SESSION_ID'] = session_id
411
+ tags = entry ? entry['tags'].join(',') : ''
412
+ ENV['CC_RESUME_TAGS'] = tags unless tags.empty?
413
+
374
414
  if session_id.start_with?('path:')
375
415
  exec('claude', '-c')
376
416
  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,74 @@ 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 env var session ID, then detected, then path-based
174
+ key = original_id || session_id || "path:#{cwd}"
91
175
  bookmarks['sessions'][key] = { 'path' => cwd, 'tags' => tags }
92
176
  save_bookmarks(bookmarks)
93
177
  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.3.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-02-27 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.3.0: Running session indicator, resume
16
+ breadcrumbs, env-var session tracking.'
17
17
  email:
18
18
  - g@isene.com
19
19
  executables: