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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +1 -0
- data/bin/cc +43 -3
- data/bin/cc-bookmark +93 -9
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 605f4de2854d8c09e7930c20cb0bce3f3b511d6dc4d7f7e87258d1eb9082108f
|
|
4
|
+
data.tar.gz: c87c3a2b615a1631c5932154055b49d8bebec23663031dd1ffb446fe79477cef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
16
|
-
|
|
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:
|