cc-sessions 1.1.3 → 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 +20 -0
- data/bin/cc +149 -87
- data/bin/cc-bookmark +73 -13
- 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,25 @@
|
|
|
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
|
+
|
|
17
|
+
## [1.1.4] - 2025-02-10
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Bookmark path resolution when claude changes directory during session
|
|
21
|
+
- `cc -C` now resolves original session directory via ~/.claude/projects/
|
|
22
|
+
|
|
3
23
|
## [1.1.3] - 2025-02-04
|
|
4
24
|
|
|
5
25
|
### Added
|
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,69 +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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
def resolve_session_dir(cwd)
|
|
75
|
+
path = cwd
|
|
76
|
+
loop do
|
|
77
|
+
encoded = path.gsub('/', '-')
|
|
78
|
+
return path if Dir.exist?(File.join(CLAUDE_PROJECTS, encoded))
|
|
79
|
+
parent = File.dirname(path)
|
|
80
|
+
break if parent == path
|
|
81
|
+
path = parent
|
|
82
|
+
end
|
|
83
|
+
cwd
|
|
82
84
|
end
|
|
83
85
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
|
|
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)
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
bookmarks = load_bookmarks
|
|
98
|
-
|
|
99
|
-
puts "Currently running Claude Code sessions:\n\n"
|
|
96
|
+
File.basename(newest, '.jsonl')
|
|
97
|
+
end
|
|
100
98
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
def load_bookmarks
|
|
100
|
+
unless File.exist?(BOOKMARKS_FILE)
|
|
101
|
+
return { 'version' => 2, 'sessions' => {} }
|
|
102
|
+
end
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
110
113
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
puts
|
|
118
|
-
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 }
|
|
119
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))
|
|
120
129
|
end
|
|
121
130
|
|
|
122
131
|
def find_session_by_tag(tag)
|
|
123
132
|
bookmarks = load_bookmarks
|
|
124
|
-
bookmarks.each do |
|
|
125
|
-
|
|
133
|
+
bookmarks['sessions'].each do |id, entry|
|
|
134
|
+
if entry['tags'].include?(tag)
|
|
135
|
+
return { id: id, path: entry['path'] }
|
|
136
|
+
end
|
|
126
137
|
end
|
|
127
138
|
nil
|
|
128
139
|
end
|
|
129
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
|
+
|
|
130
148
|
def session_exists_in_dir?(dir)
|
|
131
|
-
# Check for .claude directory with session files
|
|
132
149
|
claude_dir = File.join(dir, '.claude')
|
|
133
150
|
return false unless Dir.exist?(claude_dir)
|
|
134
|
-
|
|
135
|
-
# Look for conversation files or other session indicators
|
|
136
151
|
Dir.glob(File.join(claude_dir, '**/*')).any? { |f| File.file?(f) }
|
|
137
152
|
end
|
|
138
153
|
|
|
@@ -140,32 +155,30 @@ def read_key
|
|
|
140
155
|
input = $stdin.getch
|
|
141
156
|
if input == "\e"
|
|
142
157
|
begin
|
|
143
|
-
input << $stdin.read_nonblock(
|
|
158
|
+
input << $stdin.read_nonblock(5)
|
|
144
159
|
rescue IO::WaitReadable
|
|
145
160
|
end
|
|
146
161
|
end
|
|
147
162
|
case input
|
|
148
|
-
when "\e[A", "k"
|
|
149
|
-
when "\e[B", "j"
|
|
150
|
-
when "\
|
|
151
|
-
when "
|
|
152
|
-
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
|
|
153
170
|
else nil
|
|
154
171
|
end
|
|
155
172
|
end
|
|
156
173
|
|
|
157
|
-
def save_bookmarks(bookmarks)
|
|
158
|
-
File.write(BOOKMARKS_FILE, JSON.pretty_generate(bookmarks))
|
|
159
|
-
end
|
|
160
|
-
|
|
161
174
|
def delete_bookmark_by_tag(tag)
|
|
162
175
|
bookmarks = load_bookmarks
|
|
163
|
-
|
|
176
|
+
match = find_session_by_tag(tag)
|
|
164
177
|
|
|
165
|
-
if
|
|
166
|
-
bookmarks.delete(
|
|
178
|
+
if match
|
|
179
|
+
bookmarks['sessions'].delete(match[:id])
|
|
167
180
|
save_bookmarks(bookmarks)
|
|
168
|
-
puts "Deleted bookmark: #{path}"
|
|
181
|
+
puts "Deleted bookmark: #{match[:path]}"
|
|
169
182
|
puts " (was tagged: #{tag})"
|
|
170
183
|
else
|
|
171
184
|
puts "No session found with tag '#{tag}'"
|
|
@@ -176,41 +189,38 @@ end
|
|
|
176
189
|
def list_bookmarks
|
|
177
190
|
bookmarks = load_bookmarks
|
|
178
191
|
|
|
179
|
-
if bookmarks.empty?
|
|
192
|
+
if bookmarks['sessions'].empty?
|
|
180
193
|
puts "No bookmarked sessions."
|
|
181
194
|
puts
|
|
182
195
|
puts "To bookmark a session, use '/bm tag1 tag2' in Claude Code."
|
|
183
196
|
return
|
|
184
197
|
end
|
|
185
198
|
|
|
186
|
-
items = bookmarks.map do |
|
|
187
|
-
{ 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']) }
|
|
188
201
|
end
|
|
189
202
|
|
|
190
203
|
index = 0
|
|
191
|
-
puts "Select session (
|
|
204
|
+
puts "Select session (\u2191/\u2193/j/k move, J/K reorder, Enter select, d delete, q quit):\n\n"
|
|
192
205
|
|
|
193
|
-
# Hide cursor
|
|
194
206
|
print "\e[?25l"
|
|
195
207
|
|
|
196
208
|
begin
|
|
197
209
|
loop do
|
|
198
|
-
# Render list
|
|
199
210
|
items.each_with_index do |item, i|
|
|
200
211
|
print clear_line
|
|
201
212
|
tag_str = cyan(item[:tags].join(', '))
|
|
202
213
|
path_str = dim(item[:path])
|
|
203
214
|
missing = item[:exists] ? '' : red(' [NOT FOUND]')
|
|
204
|
-
line = "#{tag_str}
|
|
215
|
+
line = "#{tag_str} \u2192 #{path_str}#{missing}"
|
|
205
216
|
|
|
206
217
|
if i == index
|
|
207
|
-
puts "
|
|
218
|
+
puts "\u25b8 #{line}"
|
|
208
219
|
else
|
|
209
220
|
puts " #{line}"
|
|
210
221
|
end
|
|
211
222
|
end
|
|
212
223
|
|
|
213
|
-
# Move cursor back to start
|
|
214
224
|
print "\e[#{items.size}A"
|
|
215
225
|
|
|
216
226
|
key = read_key
|
|
@@ -219,27 +229,35 @@ def list_bookmarks
|
|
|
219
229
|
index = (index - 1) % items.size
|
|
220
230
|
when :down
|
|
221
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
|
|
222
244
|
when :delete
|
|
223
|
-
# Show confirmation
|
|
224
245
|
old_size = items.size
|
|
225
|
-
print "\e[#{old_size}B"
|
|
246
|
+
print "\e[#{old_size}B"
|
|
226
247
|
print clear_line
|
|
227
248
|
print "Delete '#{items[index][:tags].join(', ')}'? (y/n) "
|
|
228
249
|
confirm = $stdin.getch
|
|
229
250
|
if confirm.downcase == 'y'
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
items.delete_at(index)
|
|
251
|
+
deleted = items.delete_at(index)
|
|
252
|
+
rebuild_and_save(items)
|
|
233
253
|
index = [index, items.size - 1].min
|
|
234
254
|
if items.empty?
|
|
235
|
-
# Clear list and confirmation line
|
|
236
255
|
print "\e[#{old_size}A"
|
|
237
256
|
(old_size + 1).times { print clear_line; puts }
|
|
238
257
|
print "\e[#{old_size + 1}A"
|
|
239
258
|
puts "All bookmarks deleted."
|
|
240
259
|
return
|
|
241
260
|
end
|
|
242
|
-
# Clear old list (one more line than new size)
|
|
243
261
|
print clear_line
|
|
244
262
|
print "\e[#{old_size}A"
|
|
245
263
|
(old_size).times { print clear_line; puts }
|
|
@@ -249,30 +267,65 @@ def list_bookmarks
|
|
|
249
267
|
print "\e[#{old_size}A"
|
|
250
268
|
end
|
|
251
269
|
when :enter
|
|
252
|
-
# Clear the menu
|
|
253
270
|
(items.size).times { print clear_line; puts }
|
|
254
271
|
print "\e[#{items.size}A"
|
|
255
272
|
|
|
256
273
|
if items[index][:exists]
|
|
257
|
-
print "\e[?25h"
|
|
258
|
-
resume_session(items[index][:path])
|
|
274
|
+
print "\e[?25h"
|
|
275
|
+
resume_session(items[index][:id], items[index][:path])
|
|
259
276
|
else
|
|
260
277
|
puts red("Directory not found: #{items[index][:path]}")
|
|
261
278
|
end
|
|
262
279
|
return
|
|
263
280
|
when :quit
|
|
264
|
-
# Clear the menu
|
|
265
281
|
(items.size).times { print clear_line; puts }
|
|
266
282
|
print "\e[#{items.size}A"
|
|
267
283
|
return
|
|
268
284
|
end
|
|
269
285
|
end
|
|
270
286
|
ensure
|
|
271
|
-
# Always show cursor
|
|
272
287
|
print "\e[?25h"
|
|
273
288
|
end
|
|
274
289
|
end
|
|
275
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
|
+
|
|
276
329
|
def show_help
|
|
277
330
|
puts <<~HELP
|
|
278
331
|
CC - Claude Code Session Manager
|
|
@@ -307,20 +360,29 @@ def show_help
|
|
|
307
360
|
HELP
|
|
308
361
|
end
|
|
309
362
|
|
|
310
|
-
def resume_session(path)
|
|
363
|
+
def resume_session(session_id, path)
|
|
311
364
|
unless Dir.exist?(path)
|
|
312
365
|
puts "Error: Directory not found: #{path}"
|
|
313
366
|
exit 1
|
|
314
367
|
end
|
|
315
368
|
|
|
316
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"
|
|
317
373
|
puts "Resuming session in: #{path}"
|
|
318
|
-
|
|
319
|
-
|
|
374
|
+
if session_id.start_with?('path:')
|
|
375
|
+
exec('claude', '-c')
|
|
376
|
+
else
|
|
377
|
+
exec('claude', '--resume', session_id)
|
|
378
|
+
end
|
|
320
379
|
end
|
|
321
380
|
end
|
|
322
381
|
|
|
323
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"
|
|
324
386
|
if session_exists_in_dir?(Dir.pwd)
|
|
325
387
|
exec('claude', '-c')
|
|
326
388
|
else
|
|
@@ -350,19 +412,19 @@ when nil
|
|
|
350
412
|
continue_or_start
|
|
351
413
|
else
|
|
352
414
|
tag = ARGV[0]
|
|
353
|
-
|
|
415
|
+
match = find_session_by_tag(tag)
|
|
354
416
|
|
|
355
|
-
if
|
|
356
|
-
resume_session(path)
|
|
417
|
+
if match
|
|
418
|
+
resume_session(match[:id], match[:path])
|
|
357
419
|
else
|
|
358
420
|
puts "No session found with tag '#{tag}'"
|
|
359
421
|
puts
|
|
360
422
|
puts "Available tags:"
|
|
361
423
|
bookmarks = load_bookmarks
|
|
362
|
-
if bookmarks.empty?
|
|
424
|
+
if bookmarks['sessions'].empty?
|
|
363
425
|
puts " (none - use '/bm tag1 tag2' in Claude Code to bookmark)"
|
|
364
426
|
else
|
|
365
|
-
all_tags = bookmarks.values.
|
|
427
|
+
all_tags = bookmarks['sessions'].values.flat_map { |e| e['tags'] }.uniq.sort
|
|
366
428
|
all_tags.each { |t| puts " #{t}" }
|
|
367
429
|
end
|
|
368
430
|
exit 1
|
data/bin/cc-bookmark
CHANGED
|
@@ -2,34 +2,94 @@
|
|
|
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'
|
|
9
9
|
|
|
10
10
|
CONFIG_DIR = File.expand_path('~/.cc-sessions')
|
|
11
11
|
BOOKMARKS_FILE = File.join(CONFIG_DIR, 'bookmarks.json')
|
|
12
|
+
CLAUDE_PROJECTS = File.expand_path('~/.claude/projects')
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
def resolve_session_dir(cwd)
|
|
15
|
+
path = cwd
|
|
16
|
+
loop do
|
|
17
|
+
encoded = path.gsub('/', '-')
|
|
18
|
+
return path if Dir.exist?(File.join(CLAUDE_PROJECTS, encoded))
|
|
19
|
+
parent = File.dirname(path)
|
|
20
|
+
break if parent == path
|
|
21
|
+
path = parent
|
|
22
|
+
end
|
|
23
|
+
cwd
|
|
24
|
+
end
|
|
25
|
+
|
|
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
|
|
35
|
+
|
|
36
|
+
File.basename(newest, '.jsonl')
|
|
37
|
+
end
|
|
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
|
|
14
64
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
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
|
|
20
69
|
|
|
21
|
-
|
|
70
|
+
FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
|
|
71
|
+
|
|
72
|
+
bookmarks = load_bookmarks
|
|
73
|
+
cwd = resolve_session_dir(Dir.pwd)
|
|
74
|
+
session_id = detect_session_id(cwd)
|
|
22
75
|
tags = ARGV
|
|
23
76
|
|
|
24
77
|
if tags.empty?
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
27
86
|
else
|
|
28
|
-
puts "
|
|
87
|
+
puts "Could not detect session ID. Usage: /bm tag1 tag2 ..."
|
|
29
88
|
end
|
|
30
89
|
else
|
|
31
|
-
|
|
32
|
-
|
|
90
|
+
key = session_id || "path:#{cwd}"
|
|
91
|
+
bookmarks['sessions'][key] = { 'path' => cwd, 'tags' => tags }
|
|
92
|
+
save_bookmarks(bookmarks)
|
|
33
93
|
puts "Bookmarked: #{cwd}"
|
|
34
94
|
puts "Tags: #{tags.join(', ')}"
|
|
35
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:
|