@procrastivity/clast 0.0.2 → 0.0.4

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.
@@ -1,32 +1,22 @@
1
- #!/usr/bin/env bash
2
- # clast-wake — LLM-powered day curation without Claude Code.
1
+ # clast wake — LLM-powered interactive day curation.
3
2
  #
4
- # Replicates the /day-wakeup plugin skill using an OpenAI-compatible
5
- # chat completions endpoint. Calls clast commands for data, assembles
6
- # prompts, calls the LLM via curl, presents drafts interactively.
3
+ # Replicates the /day-wakeup plugin skill using an OpenAI-compatible chat
4
+ # completions endpoint. Calls clast-plumbing for data, assembles prompts,
5
+ # calls the LLM via curl, presents drafts interactively.
7
6
  #
8
- # Required env vars:
9
- # CLAST_LLM_BASE_URL — e.g. https://api.openai.com/v1
10
- # CLAST_LLM_API_KEY — bearer token
11
- # CLAST_LLM_MODEL — e.g. gpt-4o, llama3
12
- set -euo pipefail
7
+ # Usage: clast wake
8
+ # shellcheck shell=bash
13
9
 
14
- CLAST_WAKE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
10
+ # --- Small helpers -----------------------------------------------------------
15
11
 
16
- # ── Helpers ──────────────────────────────────────────────────────────
17
-
18
- die() { printf 'clast-wake: %s\n' "$1" >&2; exit "${2:-1}"; }
19
- warn() { printf 'clast-wake: warning: %s\n' "$1" >&2; }
20
- info() { printf '%s\n' "$1"; }
21
-
22
- slugify() {
12
+ _clast_wake_slugify() {
23
13
  local s="$1"
24
14
  s="${s,,}"
25
15
  s="$(printf '%s' "$s" | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//')"
26
16
  printf '%s' "${s:0:60}"
27
17
  }
28
18
 
29
- separator() {
19
+ _clast_wake_separator() {
30
20
  local label="$1"
31
21
  local width=72
32
22
  local pad=$(( width - ${#label} - 4 ))
@@ -37,141 +27,25 @@ separator() {
37
27
  printf '── %s %s\n' "$label" "$dashes"
38
28
  }
39
29
 
40
- # ── Preflight ────────────────────────────────────────────────────────
30
+ # --- Preflight ---------------------------------------------------------------
41
31
 
42
- preflight() {
32
+ _clast_wake_preflight() {
43
33
  if [[ ! -t 0 ]]; then
44
- die "clast-wake requires an interactive terminal (stdin is not a tty)."
45
- fi
46
-
47
- for tool in clast curl jq; do
48
- if ! command -v "$tool" >/dev/null 2>&1; then
49
- die "required tool not found: $tool"
50
- fi
51
- done
52
-
53
- local missing=0
54
- if [[ -z "${CLAST_LLM_BASE_URL:-}" ]]; then
55
- warn "CLAST_LLM_BASE_URL not set"; missing=1
56
- fi
57
- if [[ -z "${CLAST_LLM_API_KEY:-}" ]]; then
58
- warn "CLAST_LLM_API_KEY not set"; missing=1
59
- fi
60
- if [[ -z "${CLAST_LLM_MODEL:-}" ]]; then
61
- warn "CLAST_LLM_MODEL not set"; missing=1
62
- fi
63
-
64
- if (( missing )); then
65
- cat >&2 <<'EOF'
66
-
67
- Set these env vars before running clast-wake:
68
-
69
- export CLAST_LLM_BASE_URL="https://api.openai.com/v1"
70
- export CLAST_LLM_API_KEY="sk-..."
71
- export CLAST_LLM_MODEL="gpt-4o"
72
-
73
- Or for a local model (ollama, vllm, etc.):
74
-
75
- export CLAST_LLM_BASE_URL="http://localhost:11434/v1"
76
- export CLAST_LLM_API_KEY="unused"
77
- export CLAST_LLM_MODEL="llama3"
78
- EOF
79
- exit 1
80
- fi
81
- }
82
-
83
- # ── LLM call ─────────────────────────────────────────────────────────
84
-
85
- llm_chat() {
86
- local system_msg="$1"
87
- local user_msg="$2"
88
-
89
- local payload
90
- payload="$(jq -cn \
91
- --arg model "$CLAST_LLM_MODEL" \
92
- --arg system "$system_msg" \
93
- --arg user "$user_msg" \
94
- '{
95
- model: $model,
96
- messages: [
97
- {role: "system", content: $system},
98
- {role: "user", content: $user}
99
- ],
100
- temperature: 0.3
101
- }')"
102
-
103
- local response http_code body
104
- response="$(curl -s -w '\n%{http_code}' \
105
- "${CLAST_LLM_BASE_URL}/chat/completions" \
106
- -H "Authorization: Bearer $CLAST_LLM_API_KEY" \
107
- -H "Content-Type: application/json" \
108
- -d "$payload" 2>&1)" || true
109
-
110
- http_code="$(tail -n1 <<<"$response")"
111
- body="$(sed '$d' <<<"$response")"
112
-
113
- if [[ "$http_code" != "200" ]]; then
114
- warn "LLM API returned HTTP $http_code"
115
- if [[ -n "$body" ]]; then
116
- local err_msg
117
- err_msg="$(jq -r '.error.message // .error // .' <<<"$body" 2>/dev/null || echo "$body")"
118
- warn "$err_msg"
119
- fi
120
- return 1
121
- fi
122
-
123
- local content
124
- content="$(jq -r '.choices[0].message.content // empty' <<<"$body" 2>/dev/null)" || {
125
- warn "failed to parse LLM response"
126
- return 1
127
- }
128
-
129
- if [[ -z "$content" ]]; then
130
- warn "LLM returned empty content"
131
- return 1
132
- fi
133
-
134
- printf '%s' "$content"
135
- }
136
-
137
- # ── Prompt template ──────────────────────────────────────────────────
138
-
139
- resolve_prompt_dir() {
140
- local dir="$CLAST_WAKE_DIR/lib/clast/prompts"
141
- if [[ -d "$dir" ]]; then
142
- printf '%s' "$dir"
143
- return
34
+ clast_porcelain_die "clast wake requires an interactive terminal (stdin is not a tty)."
144
35
  fi
145
- local installed
146
- for installed in /usr/local/lib/clast/prompts "$HOME/.local/lib/clast/prompts"; do
147
- if [[ -d "$installed" ]]; then
148
- printf '%s' "$installed"
149
- return
150
- fi
151
- done
152
- die "cannot find prompts directory (checked $dir and install paths)"
36
+ clast_porcelain_preflight_llm
153
37
  }
154
38
 
155
- load_system_prompt() {
156
- local prompt_dir
157
- prompt_dir="$(resolve_prompt_dir)"
158
- local file="$prompt_dir/day-wakeup-draft-system.md"
159
- if [[ ! -r "$file" ]]; then
160
- die "system prompt not found: $file"
161
- fi
162
- cat "$file"
163
- }
39
+ # --- User prompt -------------------------------------------------------------
164
40
 
165
- build_user_prompt() {
41
+ _clast_wake_build_user_prompt() {
166
42
  local project="$1" branch="$2" start="$3" end="$4" msg_count="$5"
167
43
  local first_turns="$6" last_turns="$7" breadcrumbs="$8"
168
44
 
169
- local prompt_dir
170
- prompt_dir="$(resolve_prompt_dir)"
171
- local template_file="$prompt_dir/day-wakeup-draft-user.md"
45
+ local template_file template
46
+ template_file="$(clast_porcelain_user_prompt_file day-wakeup-draft-user)"
172
47
 
173
- if [[ -r "$template_file" ]]; then
174
- local template
48
+ if [[ -n "$template_file" ]]; then
175
49
  template="$(cat "$template_file")"
176
50
  template="${template//\{\{project\}\}/${project}}"
177
51
  template="${template//\{\{branch\}\}/${branch:-unknown}}"
@@ -183,7 +57,7 @@ build_user_prompt() {
183
57
  template="${template//\{\{breadcrumbs\}\}/${breadcrumbs:-None.}}"
184
58
  printf '%s' "$template"
185
59
  else
186
- warn "user prompt template not found: $template_file — using inline fallback"
60
+ clast_porcelain_warn "user prompt template not found: day-wakeup-draft-user.md — using inline fallback"
187
61
  cat <<EOF
188
62
  Session metadata:
189
63
  - Project: ${project}
@@ -204,34 +78,31 @@ EOF
204
78
  fi
205
79
  }
206
80
 
207
- # ── Draft parsing ────────────────────────────────────────────────────
81
+ # --- Draft parsing -----------------------------------------------------------
208
82
 
209
- extract_title() {
83
+ _clast_wake_extract_title() {
210
84
  local draft="$1"
211
- local title
212
- title="$(grep -m1 '^# Session:' <<<"$draft" | sed 's/^# Session:[[:space:]]*//')"
213
- printf '%s' "$title"
85
+ grep -m1 '^# Session:' <<<"$draft" | sed 's/^# Session:[[:space:]]*//'
214
86
  }
215
87
 
216
- extract_tags() {
88
+ _clast_wake_extract_tags() {
217
89
  local draft="$1"
218
90
  local tags
219
91
  tags="$(grep -i '^Suggested tags:' <<<"$draft" | sed 's/^[Ss]uggested tags:[[:space:]]*//')"
220
- tags="$(printf '%s' "$tags" | sed 's/[[:space:]]*,[[:space:]]*/,/g')"
221
- printf '%s' "$tags"
92
+ printf '%s' "$tags" | sed 's/[[:space:]]*,[[:space:]]*/,/g'
222
93
  }
223
94
 
224
- strip_tags_trailer() {
95
+ _clast_wake_strip_tags_trailer() {
225
96
  local draft="$1"
226
97
  printf '%s' "$draft" | sed '/^$/{ N; /\n[Ss]uggested tags:/d; }' | sed '/^[Ss]uggested tags:/d'
227
98
  }
228
99
 
229
- # ── Session slug ─────────────────────────────────────────────────────
100
+ # --- Session slug ------------------------------------------------------------
230
101
 
231
- get_session_slug() {
102
+ _clast_wake_get_session_slug() {
232
103
  local snapshot_path="$1" draft_title="$2"
233
104
  local journal_dir
234
- journal_dir="$(clast whereami 2>/dev/null | grep '^journal_dir:' | awk '{print $2}')" || true
105
+ journal_dir="$(clast-plumbing whereami 2>/dev/null | grep '^journal_dir:' | awk '{print $2}')" || true
235
106
  if [[ -z "$journal_dir" ]]; then
236
107
  journal_dir="${HOME}/.claude/journal"
237
108
  fi
@@ -242,22 +113,22 @@ get_session_slug() {
242
113
  local ai_title
243
114
  ai_title="$(jq -r 'select(.type == "ai-title") | .aiTitle' "$abs_path" 2>/dev/null | tail -1)" || true
244
115
  if [[ -n "$ai_title" && "$ai_title" != "null" ]]; then
245
- slugify "$ai_title"
116
+ _clast_wake_slugify "$ai_title"
246
117
  return
247
118
  fi
248
119
  fi
249
120
 
250
121
  if [[ -n "$draft_title" ]]; then
251
- slugify "$draft_title"
122
+ _clast_wake_slugify "$draft_title"
252
123
  return
253
124
  fi
254
125
 
255
126
  printf 'session'
256
127
  }
257
128
 
258
- # ── Interactive menu ─────────────────────────────────────────────────
129
+ # --- Interactive menu --------------------------------------------------------
259
130
 
260
- prompt_choice() {
131
+ _clast_wake_prompt_choice() {
261
132
  local choice
262
133
  printf '\n [a] Accept [e] Edit [d] Dismiss [s] Skip [q] Stop here\n' >/dev/tty
263
134
  printf ' Choice: ' >/dev/tty
@@ -266,23 +137,22 @@ prompt_choice() {
266
137
  printf '%s' "$choice"
267
138
  }
268
139
 
269
- prompt_edit_feedback() {
140
+ _clast_wake_prompt_edit_feedback() {
270
141
  local feedback
271
142
  printf '\n What should change? ' >/dev/tty
272
143
  read -r feedback </dev/tty
273
144
  printf '%s' "$feedback"
274
145
  }
275
146
 
276
- # ── Triage ───────────────────────────────────────────────────────────
147
+ # --- Triage ------------------------------------------------------------------
277
148
 
278
149
  _clast_wake_triage() {
279
150
  local uncurated="$1" total="$2" day_count="$3"
280
151
  local first_day="$4" last_day="$5" project_count="$6"
281
152
 
282
- info "Found $total uncurated session(s) across $day_count day(s) ($first_day to $last_day)." >/dev/tty
153
+ clast_porcelain_info "Found $total uncurated session(s) across $day_count day(s) ($first_day to $last_day)." >/dev/tty
283
154
  printf '\n' >/dev/tty
284
155
 
285
- # Show per-day breakdown
286
156
  local breakdown
287
157
  breakdown="$(jq -r '
288
158
  group_by(.day_bucket) | sort_by(.[0].day_bucket) | .[] |
@@ -315,13 +185,13 @@ _clast_wake_triage() {
315
185
  printf ' How many days back? ' >/dev/tty
316
186
  read -r days </dev/tty
317
187
  if ! [[ "$days" =~ ^[1-9][0-9]*$ ]]; then
318
- warn "invalid number — processing all sessions"
188
+ clast_porcelain_warn "invalid number — processing all sessions"
319
189
  printf '%s' "$uncurated"
320
190
  return
321
191
  fi
322
192
  local cutoff
323
193
  cutoff="$(date -d "-${days} days" +%Y-%m-%d 2>/dev/null)" || {
324
- warn "failed to compute date — processing all sessions"
194
+ clast_porcelain_warn "failed to compute date — processing all sessions"
325
195
  printf '%s' "$uncurated"
326
196
  return
327
197
  }
@@ -332,28 +202,27 @@ _clast_wake_triage() {
332
202
  printf ' Keep how many days? (dismiss everything older) ' >/dev/tty
333
203
  read -r days_keep </dev/tty
334
204
  if ! [[ "$days_keep" =~ ^[1-9][0-9]*$ ]]; then
335
- warn "invalid number — processing all sessions"
205
+ clast_porcelain_warn "invalid number — processing all sessions"
336
206
  printf '%s' "$uncurated"
337
207
  return
338
208
  fi
339
209
  local cutoff_keep
340
210
  cutoff_keep="$(date -d "-${days_keep} days" +%Y-%m-%d 2>/dev/null)" || {
341
- warn "failed to compute date — processing all sessions"
211
+ clast_porcelain_warn "failed to compute date — processing all sessions"
342
212
  printf '%s' "$uncurated"
343
213
  return
344
214
  }
345
- # Dismiss older sessions
346
215
  local old_ids
347
216
  old_ids="$(jq -r "[.[] | select(.day_bucket < \"$cutoff_keep\")] | .[].session_id" <<<"$uncurated")"
348
217
  local dismiss_count=0
349
218
  local old_id
350
219
  while IFS= read -r old_id; do
351
220
  [[ -z "$old_id" ]] && continue
352
- clast sessions dismiss "$old_id" --reason "bulk dismiss via clast-wake triage" 2>/dev/null
221
+ clast-plumbing sessions dismiss "$old_id" --reason "bulk dismiss via clast wake triage" 2>/dev/null
353
222
  dismiss_count=$(( dismiss_count + 1 ))
354
223
  done <<<"$old_ids"
355
224
  if (( dismiss_count > 0 )); then
356
- info " Dismissed $dismiss_count older session(s)." >/dev/tty
225
+ clast_porcelain_info " Dismissed $dismiss_count older session(s)." >/dev/tty
357
226
  fi
358
227
  jq -c "[.[] | select(.day_bucket >= \"$cutoff_keep\")]" <<<"$uncurated"
359
228
  ;;
@@ -361,26 +230,29 @@ _clast_wake_triage() {
361
230
  printf '[]'
362
231
  ;;
363
232
  *)
364
- warn "unknown choice — processing all sessions"
233
+ clast_porcelain_warn "unknown choice — processing all sessions"
365
234
  printf '%s' "$uncurated"
366
235
  ;;
367
236
  esac
368
237
  }
369
238
 
370
- # ── Main ─────────────────────────────────────────────────────────────
239
+ # --- Main --------------------------------------------------------------------
371
240
 
372
- main() {
373
- preflight
241
+ clast_cmd_wake() {
242
+ _clast_wake_preflight
374
243
 
375
- info "Snapshotting fresh transcripts..."
376
- if ! clast snapshot 2>/dev/null; then
377
- warn "clast snapshot failed — proceeding with existing data"
244
+ clast_porcelain_info "Snapshotting fresh transcripts..."
245
+ if ! clast-plumbing snapshot 2>/dev/null; then
246
+ clast_porcelain_warn "clast-plumbing snapshot failed — proceeding with existing data"
378
247
  fi
379
248
 
380
- info "Checking for uncurated or stale sessions..."
249
+ clast_porcelain_info "Checking for uncurated or stale sessions..."
250
+ # Window is configurable; a shorter default keeps the scan fast on large
251
+ # journals (clast sessions cost scales with sessions in the window).
252
+ local since="${CLAST_WAKE_SINCE:--14d}"
381
253
  local sessions_json
382
- sessions_json="$(clast --json sessions --since -30d 2>/dev/null)" || {
383
- die "failed to list sessions"
254
+ sessions_json="$(clast-plumbing --json sessions --since "$since" 2>/dev/null)" || {
255
+ clast_porcelain_die "failed to list sessions"
384
256
  }
385
257
 
386
258
  local uncurated
@@ -389,8 +261,8 @@ main() {
389
261
  total="$(jq 'length' <<<"$uncurated")"
390
262
 
391
263
  if (( total == 0 )); then
392
- info "Nothing to curate — all sessions are curated or dismissed."
393
- exit 0
264
+ clast_porcelain_info "Nothing to curate — all sessions are curated or dismissed."
265
+ return 0
394
266
  fi
395
267
 
396
268
  local day_count first_day last_day project_count
@@ -403,13 +275,13 @@ main() {
403
275
  uncurated="$(_clast_wake_triage "$uncurated" "$total" "$day_count" "$first_day" "$last_day" "$project_count")"
404
276
  total="$(jq 'length' <<<"$uncurated")"
405
277
  if (( total == 0 )); then
406
- info "Nothing left to curate."
407
- exit 0
278
+ clast_porcelain_info "Nothing left to curate."
279
+ return 0
408
280
  fi
409
281
  project_count="$(jq '[.[].project] | unique | length' <<<"$uncurated")"
410
282
  fi
411
283
 
412
- info "Processing $total session(s) across $project_count project(s)."
284
+ clast_porcelain_info "Processing $total session(s) across $project_count project(s)."
413
285
  printf '\n'
414
286
 
415
287
  local curated_count=0 skipped_count=0 dismissed_count=0
@@ -440,12 +312,12 @@ main() {
440
312
  [[ -n "$branch" && "$branch" != "null" ]] && label="$label, $branch"
441
313
  label="$label)"
442
314
 
443
- separator "$label"
444
- info "Gathering context..."
315
+ _clast_wake_separator "$label"
316
+ clast_porcelain_info "Gathering context..."
445
317
 
446
318
  local show_json
447
- show_json="$(clast --json show "$sid" --full --turns 8 2>/dev/null)" || {
448
- warn "failed to read session $sid — skipping"
319
+ show_json="$(clast-plumbing --json show "$sid" --full --turns 8 2>/dev/null)" || {
320
+ clast_porcelain_warn "failed to read session $sid — skipping"
449
321
  skipped_count=$(( skipped_count + 1 ))
450
322
  i=$(( i + 1 ))
451
323
  continue
@@ -463,14 +335,14 @@ main() {
463
335
  ' <<<"$show_json" 2>/dev/null)" || true
464
336
 
465
337
  local breadcrumbs=""
466
- breadcrumbs="$(clast breadcrumb --read --project "$project" --day yesterday 2>/dev/null)" || true
338
+ breadcrumbs="$(clast-plumbing breadcrumb --read --project "$project" --day yesterday 2>/dev/null)" || true
467
339
 
468
340
  local user_prompt
469
- user_prompt="$(build_user_prompt "$project" "$branch" "$start_ts" "$end_ts" "$msg_count" \
341
+ user_prompt="$(_clast_wake_build_user_prompt "$project" "$branch" "$start_ts" "$end_ts" "$msg_count" \
470
342
  "$first_turns" "$last_turns" "$breadcrumbs")"
471
343
 
472
344
  local system_prompt
473
- system_prompt="$(load_system_prompt)"
345
+ system_prompt="$(clast_porcelain_load_system_prompt day-wakeup-draft-system)"
474
346
 
475
347
  local draft="" edit_extra=""
476
348
  local drafting=1
@@ -484,10 +356,10 @@ main() {
484
356
  Revisions requested by user: ${edit_extra}"
485
357
  fi
486
358
 
487
- info "Generating draft..."
488
- if ! draft="$(llm_chat "$full_system" "$full_user")"; then
359
+ clast_porcelain_info "Generating draft..."
360
+ if ! draft="$(clast_porcelain_llm_chat "$full_system" "$full_user")"; then
489
361
  printf '\n'
490
- warn "LLM call failed for session $sid"
362
+ clast_porcelain_warn "LLM call failed for session $sid"
491
363
  local retry
492
364
  printf ' [r] Retry [s] Skip [q] Stop\n Choice: '
493
365
  read -r -n1 retry </dev/tty
@@ -503,39 +375,39 @@ Revisions requested by user: ${edit_extra}"
503
375
  printf '\n'
504
376
 
505
377
  local choice
506
- choice="$(prompt_choice)"
378
+ choice="$(_clast_wake_prompt_choice)"
507
379
 
508
380
  case "$choice" in
509
381
  a|A)
510
382
  local title tags body slug
511
- title="$(extract_title "$draft")"
512
- tags="$(extract_tags "$draft")"
513
- body="$(strip_tags_trailer "$draft")"
514
- slug="$(get_session_slug "$snapshot_path" "$title")"
383
+ title="$(_clast_wake_extract_title "$draft")"
384
+ tags="$(_clast_wake_extract_tags "$draft")"
385
+ body="$(_clast_wake_strip_tags_trailer "$draft")"
386
+ slug="$(_clast_wake_get_session_slug "$snapshot_path" "$title")"
515
387
 
516
388
  local write_args=(entries write --session "$sid" --slug "$slug" --body-stdin)
517
389
  [[ -n "$tags" ]] && write_args+=(--tags "$tags")
518
390
  [[ -n "$title" ]] && write_args+=(--title "$title")
519
391
 
520
392
  local write_result
521
- if write_result="$(printf '%s\n' "$body" | clast "${write_args[@]}" 2>&1)"; then
522
- info " $write_result"
393
+ if write_result="$(printf '%s\n' "$body" | clast-plumbing "${write_args[@]}" 2>&1)"; then
394
+ clast_porcelain_info " $write_result"
523
395
  curated_count=$(( curated_count + 1 ))
524
396
  curated_projects+=("$project")
525
397
  else
526
- warn "failed to write entry: $write_result"
398
+ clast_porcelain_warn "failed to write entry: $write_result"
527
399
  fi
528
400
  drafting=0
529
401
  ;;
530
402
  e|E)
531
- edit_extra="$(prompt_edit_feedback)"
403
+ edit_extra="$(_clast_wake_prompt_edit_feedback)"
532
404
  ;;
533
405
  d|D)
534
- if clast sessions dismiss "$sid" --reason "dismissed via clast-wake" 2>/dev/null; then
535
- info " Dismissed."
406
+ if clast-plumbing sessions dismiss "$sid" --reason "dismissed via clast wake" 2>/dev/null; then
407
+ clast_porcelain_info " Dismissed."
536
408
  dismissed_count=$(( dismissed_count + 1 ))
537
409
  else
538
- warn "failed to dismiss session $sid"
410
+ clast_porcelain_warn "failed to dismiss session $sid"
539
411
  fi
540
412
  drafting=0
541
413
  ;;
@@ -548,7 +420,7 @@ Revisions requested by user: ${edit_extra}"
548
420
  drafting=0
549
421
  ;;
550
422
  *)
551
- info " Unknown choice '$choice' — skipping."
423
+ clast_porcelain_info " Unknown choice '$choice' — skipping."
552
424
  skipped_count=$(( skipped_count + 1 ))
553
425
  drafting=0
554
426
  ;;
@@ -565,15 +437,13 @@ Revisions requested by user: ${edit_extra}"
565
437
  [[ -z "$unique_projects" || "$unique_projects" == "0" ]] && unique_projects=0
566
438
 
567
439
  printf '\n'
568
- separator "Summary"
569
- info " Curated: $curated_count session(s) across $unique_projects project(s)"
440
+ _clast_wake_separator "Summary"
441
+ clast_porcelain_info " Curated: $curated_count session(s) across $unique_projects project(s)"
570
442
  if (( dismissed_count > 0 )); then
571
- info " Dismissed: $dismissed_count session(s)"
443
+ clast_porcelain_info " Dismissed: $dismissed_count session(s)"
572
444
  fi
573
- info " Skipped: $skipped_count session(s)"
445
+ clast_porcelain_info " Skipped: $skipped_count session(s)"
574
446
  if (( remaining > 0 )); then
575
- info " Remaining: $remaining session(s) (stopped early)"
447
+ clast_porcelain_info " Remaining: $remaining session(s) (stopped early)"
576
448
  fi
577
449
  }
578
-
579
- main "$@"
@@ -61,25 +61,24 @@ clast_registry_resolve() {
61
61
  local arr
62
62
  arr="$(clast_registry_list_json)"
63
63
 
64
- # Segment input: starts with `-`. Try raw segment first (handles
65
- # segments registered as-is), then decode to filesystem paths.
64
+ # Segment input: starts with `-`. The candidate set is the raw segment
65
+ # (handles segments registered as-is) followed by every dash-decoded
66
+ # filesystem path. Test them all in ONE jq pass — first candidate in
67
+ # order to match a registry .path/.aliases wins — instead of forking one
68
+ # jq per candidate, which for deep paths meant ~hundreds/thousands of
69
+ # forks per resolve (the dominant cost of `clast wake` startup).
66
70
  if [[ "$input" == -* ]]; then
67
- local slug
68
- slug="$(_clast_registry_lookup_path "$input" "$arr" 2>/dev/null)" || true
71
+ local -a candidates=("$input")
72
+ local -a decoded=()
73
+ mapfile -t decoded < <(clast_decode_candidates "$input")
74
+ candidates+=("${decoded[@]}")
75
+ local cands_json slug
76
+ cands_json="$(printf '%s\n' "${candidates[@]}" | jq -Rn '[inputs]')"
77
+ slug="$(_clast_registry_lookup_paths "$cands_json" "$arr" 2>/dev/null)" || true
69
78
  if [[ -n "$slug" ]]; then
70
79
  printf '%s\n' "$slug"
71
80
  return 0
72
81
  fi
73
- local -a candidates=()
74
- mapfile -t candidates < <(clast_decode_candidates "$input")
75
- local c
76
- for c in "${candidates[@]}"; do
77
- slug="$(_clast_registry_lookup_path "$c" "$arr")" || continue
78
- if [[ -n "$slug" ]]; then
79
- printf '%s\n' "$slug"
80
- return 0
81
- fi
82
- done
83
82
  return 1
84
83
  fi
85
84
 
@@ -107,6 +106,25 @@ _clast_registry_lookup_path() {
107
106
  ' <<<"$arr"
108
107
  }
109
108
 
109
+ # _clast_registry_lookup_paths <candidates-json-array> <registry-json-array>
110
+ # Batched form of _clast_registry_lookup_path: given an ordered JSON array
111
+ # of candidate paths, return the slug for the FIRST candidate (in order)
112
+ # that matches a registry entry's .path or .aliases — path before alias,
113
+ # matching the single-path helper. Print slug or empty. One jq pass for
114
+ # the whole candidate set.
115
+ _clast_registry_lookup_paths() {
116
+ local cands_json="$1" arr="$2"
117
+ jq -r --argjson cands "$cands_json" '
118
+ . as $arr
119
+ | first(
120
+ $cands[] as $c
121
+ | ( ($arr | map(select(.path == $c)) | .[0].slug)
122
+ // ($arr | map(select((.aliases? // []) | index($c) != null)) | .[0].slug) )
123
+ | select(. != null)
124
+ ) // empty
125
+ ' <<<"$arr"
126
+ }
127
+
110
128
  # clast_registry_add <path> [--slug NAME] [--remote URL]
111
129
  # Append a single JSONL line. Default slug = basename(path). Default
112
130
  # remote = `git -C <path> remote get-url origin` (or absent). If the
@@ -262,24 +262,20 @@ _clast_entries_list() {
262
262
  return 0
263
263
  fi
264
264
 
265
- printf '%-11s %-6s %-17s %-29s %s\n' \
266
- "date" "time" "project" "slug" "tags"
265
+ printf '%-50s %s\n' "entry" "tags"
267
266
 
268
- local n i row r_date r_time r_project r_slug r_tags tags_disp
267
+ local n i row r_path r_basename r_tags tags_disp
269
268
  n="$(jq 'length' <<<"$rows_json")"
270
269
  for (( i = 0; i < n; i++ )); do
271
270
  row="$(jq -c ".[$i]" <<<"$rows_json")"
272
- r_date="$(jq -r '.date // ""' <<<"$row")"
273
- r_time="$(jq -r '.time // ""' <<<"$row")"
274
- r_project="$(jq -r '.project // ""' <<<"$row")"
275
- r_slug="$(jq -r '.session_slug // ""' <<<"$row")"
271
+ r_path="$(jq -r '.path // ""' <<<"$row")"
272
+ r_basename="$(basename "$r_path")"
276
273
  r_tags="$(jq -r '.tags // [] | join(",")' <<<"$row")"
277
274
  tags_disp="$r_tags"
278
275
  if (( ${#tags_disp} > 30 )); then
279
276
  tags_disp="${tags_disp:0:29}…"
280
277
  fi
281
- printf '%-11s %-6s %-17s %-29s %s\n' \
282
- "$r_date" "$r_time" "$r_project" "$r_slug" "$tags_disp"
278
+ printf '%-50s %s\n' "$r_basename" "$tags_disp"
283
279
  done
284
280
  }
285
281
 
@@ -515,6 +511,7 @@ _clast_entries_write() {
515
511
  trimmed="${rt#"${rt%%[![:space:]]*}"}"
516
512
  trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
517
513
  [[ -z "$trimmed" ]] && continue
514
+ trimmed="${trimmed,,}"
518
515
  if ! [[ "$trimmed" =~ ^[a-z0-9][a-z0-9-]{0,31}$ ]]; then
519
516
  _clast_entries_err "write: invalid tag '$trimmed'"; return 2
520
517
  fi
@@ -566,16 +563,13 @@ _clast_entries_write() {
566
563
  fi
567
564
 
568
565
  # Best-effort branch from snapshot.
566
+ # Claude Code stores gitBranch (camelCase) on type:"user" lines, not on line 1.
569
567
  local journal_dir snapshot_abs branch=""
570
568
  journal_dir="$(clast_journal_dir)"
571
569
  snapshot_abs="$journal_dir/$snapshot_rel"
572
570
  if [[ -r "$snapshot_abs" ]]; then
573
- branch="$(head -n1 "$snapshot_abs" 2>/dev/null | jq -r '.cwd // .git_branch // empty' 2>/dev/null || true)"
574
- # `.cwd` isn't a branch, but we keep parity with step 07's TODO: leave best-effort placeholder.
575
- # If the value looks like a path, drop it.
576
- case "$branch" in
577
- /*|"") branch="" ;;
578
- esac
571
+ branch="$(grep -m1 '"gitBranch"' "$snapshot_abs" 2>/dev/null \
572
+ | jq -r '.gitBranch // empty' 2>/dev/null || true)"
579
573
  fi
580
574
 
581
575
  local author machine