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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/bin/cc +141 -94
  4. data/bin/cc-bookmark +59 -14
  5. metadata +5 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d599c44925b47fb2fd595e9956d6591eebd6d20491fbfcf9dabe0fd15713c36c
4
- data.tar.gz: d33291906f06251ab048ecddb2e814d5344897222d0f08feb12a2ecdf0e57414
3
+ metadata.gz: a71cdec4c4feb8c680d3c29c2871393cd7af2255c3960f5a2b36eabc64fa9a7d
4
+ data.tar.gz: 03a816b0de6e0dd58017f816dc31e1c2b4acc7b77c159191dfa5f6e91faf6b6b
5
5
  SHA512:
6
- metadata.gz: ca6d6a84a451646a729dbe14657b692991e2299429f4c96133b5136e188601812b64e6302873612e095ee132da743d6ee741bb8cf17b828fa0ca3c8bb1375fb8
7
- data.tar.gz: 5ea42920ca5c79ab283409c538cedfe1f35e99858d59c8b23d507ba6730a1a4ee7d3fd67881ac3dd7e7a222b7c25a432f4c9259cf3a339e2481a6c33efcbd41b
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 # reached root
80
+ break if parent == path
95
81
  path = parent
96
82
  end
97
- cwd # fallback to cwd if no project dir found
98
- end
99
-
100
- def find_tags_for_path(path, bookmarks)
101
- bookmarks[path] || []
83
+ cwd
102
84
  end
103
85
 
104
- def show_current_sessions
105
- # Find running claude processes
106
- pids = `pgrep -x claude 2>/dev/null`.split.map(&:to_i)
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
- bookmarks = load_bookmarks
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
- puts "Running sessions:\n\n"
96
+ File.basename(newest, '.jsonl')
97
+ end
116
98
 
117
- pids.each do |pid|
118
- cwd_link = "/proc/#{pid}/cwd"
119
- next unless File.exist?(cwd_link)
99
+ def load_bookmarks
100
+ unless File.exist?(BOOKMARKS_FILE)
101
+ return { 'version' => 2, 'sessions' => {} }
102
+ end
120
103
 
121
- begin
122
- cwd = File.readlink(cwd_link)
123
- session_dir = resolve_session_dir(cwd)
124
- tags = find_tags_for_path(session_dir, bookmarks)
125
- tag_str = tags.empty? ? dim('(no tags)') : green(tags.join(', '))
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
- puts " PID #{cyan(pid.to_s)}: #{session_dir}"
128
- puts " Tags: #{tag_str}"
129
- puts
130
- rescue Errno::EACCES
131
- puts " PID #{cyan(pid.to_s)}: #{dim('(permission denied)')}"
132
- puts
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 |path, tags|
140
- return path if tags.include?(tag)
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(3)
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" then :up
164
- when "\e[B", "j" then :down
165
- when "\r", "\n" then :enter
166
- when "q", "\e" then :quit
167
- when "d" then :delete
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
- path = find_session_by_tag(tag)
176
+ match = find_session_by_tag(tag)
179
177
 
180
- if path
181
- bookmarks.delete(path)
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 |path, tags|
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 (↑/↓/j/k move, Enter select, d delete, q quit):\n\n"
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} #{path_str}#{missing}"
215
+ line = "#{tag_str} \u2192 #{path_str}#{missing}"
220
216
 
221
217
  if i == index
222
- puts " #{line}"
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" # Move below list
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
- bookmarks.delete(items[index][:path])
246
- save_bookmarks(bookmarks)
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" # Show cursor before exec
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
- # Use -c to continue most recent session in this directory (no picker)
334
- exec('claude', '-c')
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
- path = find_session_by_tag(tag)
415
+ match = find_session_by_tag(tag)
369
416
 
370
- if path
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.flatten.uniq.sort
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
- FileUtils.mkdir_p(CONFIG_DIR) unless Dir.exist?(CONFIG_DIR)
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
- bookmarks = if File.exist?(BOOKMARKS_FILE)
31
- JSON.parse(File.read(BOOKMARKS_FILE))
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
- if bookmarks[cwd]
41
- puts "Current bookmark: #{bookmarks[cwd].join(', ')}"
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 "No bookmark for this directory. Usage: /bm tag1 tag2 ..."
87
+ puts "Could not detect session ID. Usage: /bm tag1 tag2 ..."
44
88
  end
45
89
  else
46
- bookmarks[cwd] = tags
47
- File.write(BOOKMARKS_FILE, JSON.pretty_generate(bookmarks))
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.1.4
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-10 00:00:00.000000000 Z
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: