cc-sessions 1.1.4 → 1.2.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 +14 -0
- data/bin/cc +141 -94
- data/bin/cc-bookmark +59 -14
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a71cdec4c4feb8c680d3c29c2871393cd7af2255c3960f5a2b36eabc64fa9a7d
|
|
4
|
+
data.tar.gz: 03a816b0de6e0dd58017f816dc31e1c2b4acc7b77c159191dfa5f6e91faf6b6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9fc6662868b728dff1a09b8ae9962f7efed1f32166048758366a2fb72dbda40de0a432b02817a4b2a3c7e854df3e089a272abbc1da288276cf8f85c4213376f
|
|
7
|
+
data.tar.gz: 84073e656ccf0dad4aa8fd7e01ffc6768d5863dd12cda83475ec4332dbb2cd10a7f736ff9481399abc77289480a866a23847f11721663d3e24fc7f4c8abf0a1f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0] - 2026-02-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Session ID-based bookmarks (v2 format) for reliable session resume
|
|
7
|
+
- Auto-migration from v1 path-based bookmarks to v2 session IDs
|
|
8
|
+
- OSC 7 escape sequence on session resume for wezterm CWD tracking
|
|
9
|
+
- Reorder bookmarks with J/K (Shift+j/k) in interactive list
|
|
10
|
+
- `--resume <session_id>` used for direct session resume
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Bookmarks now keyed by session UUID instead of directory path
|
|
14
|
+
- `cc-bookmark` updated to detect and store session IDs
|
|
15
|
+
- Fallback to `path:<dir>` key when session ID unavailable
|
|
16
|
+
|
|
3
17
|
## [1.1.4] - 2025-02-10
|
|
4
18
|
|
|
5
19
|
### Fixed
|
data/bin/cc
CHANGED
|
@@ -32,12 +32,11 @@ COMMAND_SOURCE = File.expand_path('../commands/bm.md', __dir__)
|
|
|
32
32
|
COMMAND_DEST = File.expand_path('~/.claude/commands/bm.md')
|
|
33
33
|
SETTINGS_FILE = File.expand_path('~/.claude/settings.json')
|
|
34
34
|
BM_PERMISSION = 'Bash(cc-bookmark:*)'
|
|
35
|
+
CLAUDE_PROJECTS = File.expand_path('~/.claude/projects')
|
|
35
36
|
|
|
36
37
|
def ensure_setup
|
|
37
|
-
# Create config directory
|
|
38
38
|
FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
|
|
39
39
|
|
|
40
|
-
# Install /bm command if not present
|
|
41
40
|
unless File.exist?(COMMAND_DEST)
|
|
42
41
|
if File.exist?(COMMAND_SOURCE)
|
|
43
42
|
FileUtils.mkdir_p(File.dirname(COMMAND_DEST))
|
|
@@ -48,7 +47,6 @@ def ensure_setup
|
|
|
48
47
|
end
|
|
49
48
|
end
|
|
50
49
|
|
|
51
|
-
# Add auto-accept permission for /bm command
|
|
52
50
|
ensure_permission
|
|
53
51
|
end
|
|
54
52
|
|
|
@@ -70,84 +68,86 @@ def ensure_permission
|
|
|
70
68
|
puts
|
|
71
69
|
end
|
|
72
70
|
rescue JSON::ParserError
|
|
73
|
-
# If settings.json is malformed, skip permission setup
|
|
74
71
|
warn "Warning: Could not parse ~/.claude/settings.json - skipping permission setup"
|
|
75
72
|
end
|
|
76
73
|
|
|
77
|
-
def load_bookmarks
|
|
78
|
-
return {} unless File.exist?(BOOKMARKS_FILE)
|
|
79
|
-
JSON.parse(File.read(BOOKMARKS_FILE))
|
|
80
|
-
rescue JSON::ParserError
|
|
81
|
-
{}
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
CLAUDE_PROJECTS = File.expand_path('~/.claude/projects')
|
|
85
|
-
|
|
86
74
|
def resolve_session_dir(cwd)
|
|
87
|
-
# Find the original session directory by checking ~/.claude/projects/
|
|
88
|
-
# Claude encodes project paths as /foo/bar -> -foo-bar
|
|
89
75
|
path = cwd
|
|
90
76
|
loop do
|
|
91
77
|
encoded = path.gsub('/', '-')
|
|
92
78
|
return path if Dir.exist?(File.join(CLAUDE_PROJECTS, encoded))
|
|
93
79
|
parent = File.dirname(path)
|
|
94
|
-
break if parent == path
|
|
80
|
+
break if parent == path
|
|
95
81
|
path = parent
|
|
96
82
|
end
|
|
97
|
-
cwd
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def find_tags_for_path(path, bookmarks)
|
|
101
|
-
bookmarks[path] || []
|
|
83
|
+
cwd
|
|
102
84
|
end
|
|
103
85
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if pids.empty?
|
|
109
|
-
puts "No Claude Code sessions currently running."
|
|
110
|
-
return
|
|
111
|
-
end
|
|
86
|
+
def detect_session_id(session_dir)
|
|
87
|
+
encoded = session_dir.gsub('/', '-')
|
|
88
|
+
project_dir = File.join(CLAUDE_PROJECTS, encoded)
|
|
89
|
+
return nil unless Dir.exist?(project_dir)
|
|
112
90
|
|
|
113
|
-
|
|
91
|
+
newest = Dir.glob(File.join(project_dir, '*.jsonl'))
|
|
92
|
+
.select { |f| File.file?(f) }
|
|
93
|
+
.max_by { |f| File.mtime(f) }
|
|
94
|
+
return nil unless newest
|
|
114
95
|
|
|
115
|
-
|
|
96
|
+
File.basename(newest, '.jsonl')
|
|
97
|
+
end
|
|
116
98
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
99
|
+
def load_bookmarks
|
|
100
|
+
unless File.exist?(BOOKMARKS_FILE)
|
|
101
|
+
return { 'version' => 2, 'sessions' => {} }
|
|
102
|
+
end
|
|
120
103
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
104
|
+
data = JSON.parse(File.read(BOOKMARKS_FILE))
|
|
105
|
+
if data['version']
|
|
106
|
+
data
|
|
107
|
+
else
|
|
108
|
+
migrate_bookmarks(data)
|
|
109
|
+
end
|
|
110
|
+
rescue JSON::ParserError
|
|
111
|
+
{ 'version' => 2, 'sessions' => {} }
|
|
112
|
+
end
|
|
126
113
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
end
|
|
114
|
+
def migrate_bookmarks(old_data)
|
|
115
|
+
sessions = {}
|
|
116
|
+
old_data.each do |path, tags|
|
|
117
|
+
sid = detect_session_id(path)
|
|
118
|
+
key = sid || "path:#{path}"
|
|
119
|
+
sessions[key] = { 'path' => path, 'tags' => tags }
|
|
134
120
|
end
|
|
121
|
+
new_data = { 'version' => 2, 'sessions' => sessions }
|
|
122
|
+
save_bookmarks(new_data)
|
|
123
|
+
new_data
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def save_bookmarks(bookmarks)
|
|
127
|
+
FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
|
|
128
|
+
File.write(BOOKMARKS_FILE, JSON.pretty_generate(bookmarks))
|
|
135
129
|
end
|
|
136
130
|
|
|
137
131
|
def find_session_by_tag(tag)
|
|
138
132
|
bookmarks = load_bookmarks
|
|
139
|
-
bookmarks.each do |
|
|
140
|
-
|
|
133
|
+
bookmarks['sessions'].each do |id, entry|
|
|
134
|
+
if entry['tags'].include?(tag)
|
|
135
|
+
return { id: id, path: entry['path'] }
|
|
136
|
+
end
|
|
141
137
|
end
|
|
142
138
|
nil
|
|
143
139
|
end
|
|
144
140
|
|
|
141
|
+
def find_tags_for_path(path, bookmarks)
|
|
142
|
+
bookmarks['sessions'].each_value do |entry|
|
|
143
|
+
return entry['tags'] if entry['path'] == path
|
|
144
|
+
end
|
|
145
|
+
[]
|
|
146
|
+
end
|
|
147
|
+
|
|
145
148
|
def session_exists_in_dir?(dir)
|
|
146
|
-
# Check for .claude directory with session files
|
|
147
149
|
claude_dir = File.join(dir, '.claude')
|
|
148
150
|
return false unless Dir.exist?(claude_dir)
|
|
149
|
-
|
|
150
|
-
# Look for conversation files or other session indicators
|
|
151
151
|
Dir.glob(File.join(claude_dir, '**/*')).any? { |f| File.file?(f) }
|
|
152
152
|
end
|
|
153
153
|
|
|
@@ -155,32 +155,30 @@ def read_key
|
|
|
155
155
|
input = $stdin.getch
|
|
156
156
|
if input == "\e"
|
|
157
157
|
begin
|
|
158
|
-
input << $stdin.read_nonblock(
|
|
158
|
+
input << $stdin.read_nonblock(5)
|
|
159
159
|
rescue IO::WaitReadable
|
|
160
160
|
end
|
|
161
161
|
end
|
|
162
162
|
case input
|
|
163
|
-
when "\e[A", "k"
|
|
164
|
-
when "\e[B", "j"
|
|
165
|
-
when "\
|
|
166
|
-
when "
|
|
167
|
-
when "
|
|
163
|
+
when "\e[A", "k" then :up
|
|
164
|
+
when "\e[B", "j" then :down
|
|
165
|
+
when "\e[1;2A", "K" then :move_up
|
|
166
|
+
when "\e[1;2B", "J" then :move_down
|
|
167
|
+
when "\r", "\n" then :enter
|
|
168
|
+
when "q", "\e" then :quit
|
|
169
|
+
when "d" then :delete
|
|
168
170
|
else nil
|
|
169
171
|
end
|
|
170
172
|
end
|
|
171
173
|
|
|
172
|
-
def save_bookmarks(bookmarks)
|
|
173
|
-
File.write(BOOKMARKS_FILE, JSON.pretty_generate(bookmarks))
|
|
174
|
-
end
|
|
175
|
-
|
|
176
174
|
def delete_bookmark_by_tag(tag)
|
|
177
175
|
bookmarks = load_bookmarks
|
|
178
|
-
|
|
176
|
+
match = find_session_by_tag(tag)
|
|
179
177
|
|
|
180
|
-
if
|
|
181
|
-
bookmarks.delete(
|
|
178
|
+
if match
|
|
179
|
+
bookmarks['sessions'].delete(match[:id])
|
|
182
180
|
save_bookmarks(bookmarks)
|
|
183
|
-
puts "Deleted bookmark: #{path}"
|
|
181
|
+
puts "Deleted bookmark: #{match[:path]}"
|
|
184
182
|
puts " (was tagged: #{tag})"
|
|
185
183
|
else
|
|
186
184
|
puts "No session found with tag '#{tag}'"
|
|
@@ -191,41 +189,38 @@ end
|
|
|
191
189
|
def list_bookmarks
|
|
192
190
|
bookmarks = load_bookmarks
|
|
193
191
|
|
|
194
|
-
if bookmarks.empty?
|
|
192
|
+
if bookmarks['sessions'].empty?
|
|
195
193
|
puts "No bookmarked sessions."
|
|
196
194
|
puts
|
|
197
195
|
puts "To bookmark a session, use '/bm tag1 tag2' in Claude Code."
|
|
198
196
|
return
|
|
199
197
|
end
|
|
200
198
|
|
|
201
|
-
items = bookmarks.map do |
|
|
202
|
-
{ path: path, tags: tags, exists: Dir.exist?(path) }
|
|
199
|
+
items = bookmarks['sessions'].map do |id, entry|
|
|
200
|
+
{ id: id, path: entry['path'], tags: entry['tags'], exists: Dir.exist?(entry['path']) }
|
|
203
201
|
end
|
|
204
202
|
|
|
205
203
|
index = 0
|
|
206
|
-
puts "Select session (
|
|
204
|
+
puts "Select session (\u2191/\u2193/j/k move, J/K reorder, Enter select, d delete, q quit):\n\n"
|
|
207
205
|
|
|
208
|
-
# Hide cursor
|
|
209
206
|
print "\e[?25l"
|
|
210
207
|
|
|
211
208
|
begin
|
|
212
209
|
loop do
|
|
213
|
-
# Render list
|
|
214
210
|
items.each_with_index do |item, i|
|
|
215
211
|
print clear_line
|
|
216
212
|
tag_str = cyan(item[:tags].join(', '))
|
|
217
213
|
path_str = dim(item[:path])
|
|
218
214
|
missing = item[:exists] ? '' : red(' [NOT FOUND]')
|
|
219
|
-
line = "#{tag_str}
|
|
215
|
+
line = "#{tag_str} \u2192 #{path_str}#{missing}"
|
|
220
216
|
|
|
221
217
|
if i == index
|
|
222
|
-
puts "
|
|
218
|
+
puts "\u25b8 #{line}"
|
|
223
219
|
else
|
|
224
220
|
puts " #{line}"
|
|
225
221
|
end
|
|
226
222
|
end
|
|
227
223
|
|
|
228
|
-
# Move cursor back to start
|
|
229
224
|
print "\e[#{items.size}A"
|
|
230
225
|
|
|
231
226
|
key = read_key
|
|
@@ -234,27 +229,35 @@ def list_bookmarks
|
|
|
234
229
|
index = (index - 1) % items.size
|
|
235
230
|
when :down
|
|
236
231
|
index = (index + 1) % items.size
|
|
232
|
+
when :move_up
|
|
233
|
+
if index > 0
|
|
234
|
+
items[index], items[index - 1] = items[index - 1], items[index]
|
|
235
|
+
index -= 1
|
|
236
|
+
rebuild_and_save(items)
|
|
237
|
+
end
|
|
238
|
+
when :move_down
|
|
239
|
+
if index < items.size - 1
|
|
240
|
+
items[index], items[index + 1] = items[index + 1], items[index]
|
|
241
|
+
index += 1
|
|
242
|
+
rebuild_and_save(items)
|
|
243
|
+
end
|
|
237
244
|
when :delete
|
|
238
|
-
# Show confirmation
|
|
239
245
|
old_size = items.size
|
|
240
|
-
print "\e[#{old_size}B"
|
|
246
|
+
print "\e[#{old_size}B"
|
|
241
247
|
print clear_line
|
|
242
248
|
print "Delete '#{items[index][:tags].join(', ')}'? (y/n) "
|
|
243
249
|
confirm = $stdin.getch
|
|
244
250
|
if confirm.downcase == 'y'
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
items.delete_at(index)
|
|
251
|
+
deleted = items.delete_at(index)
|
|
252
|
+
rebuild_and_save(items)
|
|
248
253
|
index = [index, items.size - 1].min
|
|
249
254
|
if items.empty?
|
|
250
|
-
# Clear list and confirmation line
|
|
251
255
|
print "\e[#{old_size}A"
|
|
252
256
|
(old_size + 1).times { print clear_line; puts }
|
|
253
257
|
print "\e[#{old_size + 1}A"
|
|
254
258
|
puts "All bookmarks deleted."
|
|
255
259
|
return
|
|
256
260
|
end
|
|
257
|
-
# Clear old list (one more line than new size)
|
|
258
261
|
print clear_line
|
|
259
262
|
print "\e[#{old_size}A"
|
|
260
263
|
(old_size).times { print clear_line; puts }
|
|
@@ -264,30 +267,65 @@ def list_bookmarks
|
|
|
264
267
|
print "\e[#{old_size}A"
|
|
265
268
|
end
|
|
266
269
|
when :enter
|
|
267
|
-
# Clear the menu
|
|
268
270
|
(items.size).times { print clear_line; puts }
|
|
269
271
|
print "\e[#{items.size}A"
|
|
270
272
|
|
|
271
273
|
if items[index][:exists]
|
|
272
|
-
print "\e[?25h"
|
|
273
|
-
resume_session(items[index][:path])
|
|
274
|
+
print "\e[?25h"
|
|
275
|
+
resume_session(items[index][:id], items[index][:path])
|
|
274
276
|
else
|
|
275
277
|
puts red("Directory not found: #{items[index][:path]}")
|
|
276
278
|
end
|
|
277
279
|
return
|
|
278
280
|
when :quit
|
|
279
|
-
# Clear the menu
|
|
280
281
|
(items.size).times { print clear_line; puts }
|
|
281
282
|
print "\e[#{items.size}A"
|
|
282
283
|
return
|
|
283
284
|
end
|
|
284
285
|
end
|
|
285
286
|
ensure
|
|
286
|
-
# Always show cursor
|
|
287
287
|
print "\e[?25h"
|
|
288
288
|
end
|
|
289
289
|
end
|
|
290
290
|
|
|
291
|
+
def rebuild_and_save(items)
|
|
292
|
+
sessions = {}
|
|
293
|
+
items.each { |item| sessions[item[:id]] = { 'path' => item[:path], 'tags' => item[:tags] } }
|
|
294
|
+
save_bookmarks({ 'version' => 2, 'sessions' => sessions })
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def show_current_sessions
|
|
298
|
+
pids = `pgrep -x claude 2>/dev/null`.split.map(&:to_i)
|
|
299
|
+
|
|
300
|
+
if pids.empty?
|
|
301
|
+
puts "No Claude Code sessions currently running."
|
|
302
|
+
return
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
bookmarks = load_bookmarks
|
|
306
|
+
|
|
307
|
+
puts "Running sessions:\n\n"
|
|
308
|
+
|
|
309
|
+
pids.each do |pid|
|
|
310
|
+
cwd_link = "/proc/#{pid}/cwd"
|
|
311
|
+
next unless File.exist?(cwd_link)
|
|
312
|
+
|
|
313
|
+
begin
|
|
314
|
+
cwd = File.readlink(cwd_link)
|
|
315
|
+
session_dir = resolve_session_dir(cwd)
|
|
316
|
+
tags = find_tags_for_path(session_dir, bookmarks)
|
|
317
|
+
tag_str = tags.empty? ? dim('(no tags)') : green(tags.join(', '))
|
|
318
|
+
|
|
319
|
+
puts " PID #{cyan(pid.to_s)}: #{session_dir}"
|
|
320
|
+
puts " Tags: #{tag_str}"
|
|
321
|
+
puts
|
|
322
|
+
rescue Errno::EACCES
|
|
323
|
+
puts " PID #{cyan(pid.to_s)}: #{dim('(permission denied)')}"
|
|
324
|
+
puts
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
291
329
|
def show_help
|
|
292
330
|
puts <<~HELP
|
|
293
331
|
CC - Claude Code Session Manager
|
|
@@ -322,20 +360,29 @@ def show_help
|
|
|
322
360
|
HELP
|
|
323
361
|
end
|
|
324
362
|
|
|
325
|
-
def resume_session(path)
|
|
363
|
+
def resume_session(session_id, path)
|
|
326
364
|
unless Dir.exist?(path)
|
|
327
365
|
puts "Error: Directory not found: #{path}"
|
|
328
366
|
exit 1
|
|
329
367
|
end
|
|
330
368
|
|
|
331
369
|
Dir.chdir(path) do
|
|
370
|
+
# Emit OSC 7 so wezterm knows the new cwd (for Alt+n, etc.)
|
|
371
|
+
host = `hostname`.strip
|
|
372
|
+
print "\033]7;file://#{host}#{path}\007"
|
|
332
373
|
puts "Resuming session in: #{path}"
|
|
333
|
-
|
|
334
|
-
|
|
374
|
+
if session_id.start_with?('path:')
|
|
375
|
+
exec('claude', '-c')
|
|
376
|
+
else
|
|
377
|
+
exec('claude', '--resume', session_id)
|
|
378
|
+
end
|
|
335
379
|
end
|
|
336
380
|
end
|
|
337
381
|
|
|
338
382
|
def continue_or_start
|
|
383
|
+
# Emit OSC 7 so wezterm knows the cwd
|
|
384
|
+
host = `hostname`.strip
|
|
385
|
+
print "\033]7;file://#{host}#{Dir.pwd}\007"
|
|
339
386
|
if session_exists_in_dir?(Dir.pwd)
|
|
340
387
|
exec('claude', '-c')
|
|
341
388
|
else
|
|
@@ -365,19 +412,19 @@ when nil
|
|
|
365
412
|
continue_or_start
|
|
366
413
|
else
|
|
367
414
|
tag = ARGV[0]
|
|
368
|
-
|
|
415
|
+
match = find_session_by_tag(tag)
|
|
369
416
|
|
|
370
|
-
if
|
|
371
|
-
resume_session(path)
|
|
417
|
+
if match
|
|
418
|
+
resume_session(match[:id], match[:path])
|
|
372
419
|
else
|
|
373
420
|
puts "No session found with tag '#{tag}'"
|
|
374
421
|
puts
|
|
375
422
|
puts "Available tags:"
|
|
376
423
|
bookmarks = load_bookmarks
|
|
377
|
-
if bookmarks.empty?
|
|
424
|
+
if bookmarks['sessions'].empty?
|
|
378
425
|
puts " (none - use '/bm tag1 tag2' in Claude Code to bookmark)"
|
|
379
426
|
else
|
|
380
|
-
all_tags = bookmarks.values.
|
|
427
|
+
all_tags = bookmarks['sessions'].values.flat_map { |e| e['tags'] }.uniq.sort
|
|
381
428
|
all_tags.each { |t| puts " #{t}" }
|
|
382
429
|
end
|
|
383
430
|
exit 1
|
data/bin/cc-bookmark
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
# CC-Bookmark - Helper script for /bm command
|
|
5
|
-
# Bookmarks the current directory with tags
|
|
5
|
+
# Bookmarks the current directory with tags, keyed by session ID
|
|
6
6
|
|
|
7
7
|
require 'json'
|
|
8
8
|
require 'fileutils'
|
|
@@ -12,8 +12,6 @@ BOOKMARKS_FILE = File.join(CONFIG_DIR, 'bookmarks.json')
|
|
|
12
12
|
CLAUDE_PROJECTS = File.expand_path('~/.claude/projects')
|
|
13
13
|
|
|
14
14
|
def resolve_session_dir(cwd)
|
|
15
|
-
# Find the original session directory by checking ~/.claude/projects/
|
|
16
|
-
# Claude encodes project paths as /foo/bar -> -foo-bar
|
|
17
15
|
path = cwd
|
|
18
16
|
loop do
|
|
19
17
|
encoded = path.gsub('/', '-')
|
|
@@ -25,26 +23,73 @@ def resolve_session_dir(cwd)
|
|
|
25
23
|
cwd
|
|
26
24
|
end
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
def detect_session_id(session_dir)
|
|
27
|
+
encoded = session_dir.gsub('/', '-')
|
|
28
|
+
project_dir = File.join(CLAUDE_PROJECTS, encoded)
|
|
29
|
+
return nil unless Dir.exist?(project_dir)
|
|
30
|
+
|
|
31
|
+
newest = Dir.glob(File.join(project_dir, '*.jsonl'))
|
|
32
|
+
.select { |f| File.file?(f) }
|
|
33
|
+
.max_by { |f| File.mtime(f) }
|
|
34
|
+
return nil unless newest
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
else
|
|
33
|
-
{}
|
|
34
|
-
end
|
|
36
|
+
File.basename(newest, '.jsonl')
|
|
37
|
+
end
|
|
35
38
|
|
|
39
|
+
def load_bookmarks
|
|
40
|
+
return { 'version' => 2, 'sessions' => {} } unless File.exist?(BOOKMARKS_FILE)
|
|
41
|
+
|
|
42
|
+
data = JSON.parse(File.read(BOOKMARKS_FILE))
|
|
43
|
+
if data['version']
|
|
44
|
+
data
|
|
45
|
+
else
|
|
46
|
+
migrate_bookmarks(data)
|
|
47
|
+
end
|
|
48
|
+
rescue JSON::ParserError
|
|
49
|
+
{ 'version' => 2, 'sessions' => {} }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def migrate_bookmarks(old_data)
|
|
53
|
+
sessions = {}
|
|
54
|
+
old_data.each do |path, tags|
|
|
55
|
+
# Try to find a real session ID for this path
|
|
56
|
+
sid = detect_session_id(path)
|
|
57
|
+
key = sid || "path:#{path}"
|
|
58
|
+
sessions[key] = { 'path' => path, 'tags' => tags }
|
|
59
|
+
end
|
|
60
|
+
new_data = { 'version' => 2, 'sessions' => sessions }
|
|
61
|
+
save_bookmarks(new_data)
|
|
62
|
+
new_data
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def save_bookmarks(bookmarks)
|
|
66
|
+
FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
|
|
67
|
+
File.write(BOOKMARKS_FILE, JSON.pretty_generate(bookmarks))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
|
|
71
|
+
|
|
72
|
+
bookmarks = load_bookmarks
|
|
36
73
|
cwd = resolve_session_dir(Dir.pwd)
|
|
74
|
+
session_id = detect_session_id(cwd)
|
|
37
75
|
tags = ARGV
|
|
38
76
|
|
|
39
77
|
if tags.empty?
|
|
40
|
-
|
|
41
|
-
|
|
78
|
+
# Query mode: show tags for current session
|
|
79
|
+
if session_id
|
|
80
|
+
entry = bookmarks['sessions'][session_id]
|
|
81
|
+
if entry
|
|
82
|
+
puts "Current bookmark: #{entry['tags'].join(', ')}"
|
|
83
|
+
else
|
|
84
|
+
puts "No bookmark for this session. Usage: /bm tag1 tag2 ..."
|
|
85
|
+
end
|
|
42
86
|
else
|
|
43
|
-
puts "
|
|
87
|
+
puts "Could not detect session ID. Usage: /bm tag1 tag2 ..."
|
|
44
88
|
end
|
|
45
89
|
else
|
|
46
|
-
|
|
47
|
-
|
|
90
|
+
key = session_id || "path:#{cwd}"
|
|
91
|
+
bookmarks['sessions'][key] = { 'path' => cwd, 'tags' => tags }
|
|
92
|
+
save_bookmarks(bookmarks)
|
|
48
93
|
puts "Bookmarked: #{cwd}"
|
|
49
94
|
puts "Tags: #{tags.join(', ')}"
|
|
50
95
|
puts ""
|
metadata
CHANGED
|
@@ -1,18 +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.2.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-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description: A simple tool for bookmarking and resuming Claude Code sessions. Tag
|
|
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.
|
|
15
|
+
them with ''cc <tag>'' from anywhere. UPDATE v1.2.0: Session ID-based bookmarks,
|
|
16
|
+
auto-migration, OSC 7 for wezterm CWD tracking.'
|
|
16
17
|
email:
|
|
17
18
|
- g@isene.com
|
|
18
19
|
executables:
|