@procrastivity/clast 0.0.6 → 0.0.8

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,6 +1,6 @@
1
1
  {
2
2
  "name": "clast",
3
- "version": "0.1.0",
3
+ "version": "0.0.8",
4
4
  "description": "Capture, curate, and surface Claude Code session history across all your projects.",
5
5
  "homepage": "https://github.com/procrastivity/clast",
6
6
  "author": {
package/hooks/hooks.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
- "hooks": [
3
- {
4
- "event": "SessionStart",
5
- "command": "${CLAUDE_PLUGIN_ROOT}/hooks/snapshot.sh"
6
- }
7
- ]
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/snapshot.sh"
9
+ }
10
+ ]
11
+ }
12
+ ]
13
+ }
8
14
  }
@@ -21,6 +21,27 @@ clast_porcelain_info() { printf '%s\n' "$1"; }
21
21
 
22
22
  clast_porcelain_log_error() { printf 'clast: error: %s\n' "$1" >&2; }
23
23
 
24
+ # --- Timing ------------------------------------------------------------------
25
+ #
26
+ # Shared by the porcelain subcommands that time model calls (wake, retro) so
27
+ # their progress output stays consistent.
28
+
29
+ # clast_porcelain_now — epoch seconds with fraction (GNU date). Falls back to
30
+ # whole seconds where `%N` is unsupported (e.g. BSD/macOS date echoes a literal
31
+ # N). Pair two readings with clast_porcelain_elapsed.
32
+ clast_porcelain_now() {
33
+ local t
34
+ t="$(date +%s.%N 2>/dev/null)" || t="$(date +%s)"
35
+ [[ "$t" == *N* ]] && t="$(date +%s)"
36
+ printf '%s' "$t"
37
+ }
38
+
39
+ # clast_porcelain_elapsed <start> <end> — seconds between two clast_porcelain_now
40
+ # readings, one decimal place (never negative).
41
+ clast_porcelain_elapsed() {
42
+ awk -v a="$1" -v b="$2" 'BEGIN { d = b - a; if (d < 0) d = 0; printf "%.1f", d }'
43
+ }
44
+
24
45
  # --- Version / usage ---------------------------------------------------------
25
46
 
26
47
  # Reuses the plumbing's clast_version helper if available (loaded by sourcing
@@ -46,21 +46,6 @@ _clast_retro_progress() {
46
46
  fi
47
47
  }
48
48
 
49
- # _clast_retro_now — epoch seconds with fraction (GNU date). Falls back to whole
50
- # seconds where `%N` is unsupported (e.g. BSD/macOS date echoes a literal N).
51
- _clast_retro_now() {
52
- local t
53
- t="$(date +%s.%N 2>/dev/null)" || t="$(date +%s)"
54
- [[ "$t" == *N* ]] && t="$(date +%s)"
55
- printf '%s' "$t"
56
- }
57
-
58
- # _clast_retro_elapsed <start> <end> — seconds between two _clast_retro_now
59
- # readings, one decimal place (never negative).
60
- _clast_retro_elapsed() {
61
- awk -v a="$1" -v b="$2" 'BEGIN { d = b - a; if (d < 0) d = 0; printf "%.1f", d }'
62
- }
63
-
64
49
  # _clast_retrosum_fingerprint (stdin → short hex/cksum on stdout)
65
50
  # Content fingerprint of a session body; changes invalidate the cache.
66
51
  _clast_retrosum_fingerprint() {
@@ -223,12 +208,12 @@ clast_cmd_retro() {
223
208
  # Time the call so we can report how long the model took. rc distinguishes
224
209
  # a cache hit (0, instant) from a fresh model call (3) from a failure.
225
210
  rc=0
226
- t0="$(_clast_retro_now)"
211
+ t0="$(clast_porcelain_now)"
227
212
  summary="$(_clast_retrosum_summary "$project" "$work_day" "$cache_id" "$body" "$cache_dir" "$refresh")" || rc=$?
228
- t1="$(_clast_retro_now)"
213
+ t1="$(clast_porcelain_now)"
229
214
  case "$rc" in
230
215
  0) : ;; # served from cache — no model call, nothing to time
231
- 3) dt="$(_clast_retro_elapsed "$t0" "$t1")"
216
+ 3) dt="$(clast_porcelain_elapsed "$t0" "$t1")"
232
217
  queried=$(( queried + 1 ))
233
218
  model_total="$(awk -v a="$model_total" -v d="$dt" 'BEGIN { printf "%.1f", a + d }')"
234
219
  _clast_retro_progress " done in ${dt}s (model total ${model_total}s)" ;;
@@ -27,11 +27,41 @@ _clast_wake_separator() {
27
27
  printf '── %s %s\n' "$label" "$dashes"
28
28
  }
29
29
 
30
- # --- Preflight ---------------------------------------------------------------
30
+ # --- Usage / preflight -------------------------------------------------------
31
+
32
+ _clast_wake_usage() {
33
+ cat <<'EOF'
34
+ Usage: clast wake [--auto]
35
+
36
+ Interactive day curation: for each uncurated session, generate a draft journal
37
+ entry with the LLM and accept/edit/dismiss/skip it.
38
+
39
+ Flags:
40
+ --auto Non-interactive: auto-accept every generated draft and write it.
41
+ Skips the triage menu and the per-session prompt, and does not
42
+ require a tty — suitable for cron/scripts. Sessions whose draft
43
+ fails to generate are skipped. The scan window still honors
44
+ CLAST_WAKE_SINCE (default -14d).
45
+ -h, --help Print this usage and exit.
46
+
47
+ Env:
48
+ CLAST_WAKE_SINCE Scan window (default -14d).
49
+ CLAST_WAKE_AUTO_MIN_CHARS In --auto, skip drafts whose body is shorter than
50
+ this many characters (likely trivial sessions);
51
+ they stay uncurated for a later interactive pass.
52
+ Default 60. Set 0 to write every draft.
53
+
54
+ Requires the CLAST_LLM_* env vars (see `clast --help`).
55
+ EOF
56
+ }
31
57
 
58
+ # _clast_wake_preflight <auto>
59
+ # In interactive mode (auto=0) a tty is required so we can read choices; --auto
60
+ # reads nothing from the terminal, so that check is skipped.
32
61
  _clast_wake_preflight() {
33
- if [[ ! -t 0 ]]; then
34
- clast_porcelain_die "clast wake requires an interactive terminal (stdin is not a tty)."
62
+ local auto="${1:-0}"
63
+ if (( ! auto )) && [[ ! -t 0 ]]; then
64
+ clast_porcelain_die "clast wake requires an interactive terminal (stdin is not a tty). Use --auto for non-interactive curation."
35
65
  fi
36
66
  clast_porcelain_preflight_llm
37
67
  }
@@ -239,7 +269,19 @@ _clast_wake_triage() {
239
269
  # --- Main --------------------------------------------------------------------
240
270
 
241
271
  clast_cmd_wake() {
242
- _clast_wake_preflight
272
+ local auto=0
273
+ while [[ $# -gt 0 ]]; do
274
+ case "$1" in
275
+ --auto) auto=1; shift ;;
276
+ -h|--help) _clast_wake_usage; return 0 ;;
277
+ --) shift; break ;;
278
+ *) clast_porcelain_log_error "wake: unknown argument '$1'"; return 2 ;;
279
+ esac
280
+ done
281
+
282
+ _clast_wake_preflight "$auto"
283
+
284
+ (( auto )) && clast_porcelain_info "Auto mode: drafts will be accepted without review."
243
285
 
244
286
  clast_porcelain_info "Snapshotting fresh transcripts..."
245
287
  if ! clast-plumbing snapshot 2>/dev/null; then
@@ -299,7 +341,8 @@ clast_cmd_wake() {
299
341
  last_day="$(jq -r '[.[].day_bucket] | sort | last' <<<"$uncurated")"
300
342
  project_count="$(jq '[.[].project] | unique | length' <<<"$uncurated")"
301
343
 
302
- if (( day_count > 1 )); then
344
+ # Triage is an interactive scope picker; --auto processes the whole window.
345
+ if (( ! auto )) && (( day_count > 1 )); then
303
346
  uncurated="$(_clast_wake_triage "$uncurated" "$total" "$day_count" "$first_day" "$last_day" "$project_count")"
304
347
  total="$(jq 'length' <<<"$uncurated")"
305
348
  if (( total == 0 )); then
@@ -315,6 +358,15 @@ clast_cmd_wake() {
315
358
  local curated_count=0 skipped_count=0 dismissed_count=0
316
359
  local -a curated_projects=()
317
360
  local i=0 stop=0
361
+ # Cumulative model time across every draft generation (excludes the time the
362
+ # reviewer spends at the menu), so the run's LLM cost is legible — mirrors the
363
+ # per-call + total timing `clast retro` reports.
364
+ local wake_model_total="0.0"
365
+ # --auto guard: skip (do NOT write) a draft whose body is shorter than this
366
+ # many characters — likely a trivial/low-value session. Skipped sessions stay
367
+ # uncurated for a later interactive pass, so this is safe to tune aggressively.
368
+ # 0 disables the guard. Ignored in interactive mode (the reviewer decides).
369
+ local auto_min_chars="${CLAST_WAKE_AUTO_MIN_CHARS:-60}"
318
370
 
319
371
  while (( i < total && stop == 0 )); do
320
372
  local session
@@ -349,7 +401,10 @@ clast_cmd_wake() {
349
401
  local recorded="$rec_date"
350
402
  if [[ -n "$start_short" ]]; then
351
403
  recorded="$recorded $start_short"
352
- [[ -n "$end_short" && "$end_short" != "$start_short" ]] && recorded="$recorded–$end_short"
404
+ # Brace the expansion: the separator is an en dash (U+2013), and bash in a
405
+ # UTF-8 locale reads its bytes as part of the identifier — `$recorded–` then
406
+ # expands as the unset name `recorded–` and `set -u` kills the run.
407
+ [[ -n "$end_short" && "$end_short" != "$start_short" ]] && recorded="${recorded}–${end_short}"
353
408
  recorded="$recorded $tz"
354
409
  fi
355
410
 
@@ -419,9 +474,15 @@ Revisions requested by user: ${edit_extra}"
419
474
  fi
420
475
 
421
476
  clast_porcelain_info "Generating draft..."
477
+ local gen_t0 gen_t1 gen_dt
478
+ gen_t0="$(clast_porcelain_now)"
422
479
  if ! draft="$(clast_porcelain_llm_chat "$full_system" "$full_user")"; then
423
480
  printf '\n'
424
481
  clast_porcelain_warn "LLM call failed for session $sid"
482
+ # No tty to prompt in auto mode — skip this session and move on.
483
+ if (( auto )); then
484
+ skipped_count=$(( skipped_count + 1 )); break
485
+ fi
425
486
  local retry
426
487
  printf ' [r] Retry [s] Skip [q] Stop\n Choice: '
427
488
  read -r -n1 retry </dev/tty
@@ -432,12 +493,37 @@ Revisions requested by user: ${edit_extra}"
432
493
  *) skipped_count=$(( skipped_count + 1 )); break ;;
433
494
  esac
434
495
  fi
435
-
436
- printf '\n%s\n' "$draft"
437
- printf '\n'
496
+ # Only reached when the draft succeeded (every failure branch above
497
+ # continues or breaks), so this times just the model call.
498
+ gen_t1="$(clast_porcelain_now)"
499
+ gen_dt="$(clast_porcelain_elapsed "$gen_t0" "$gen_t1")"
500
+ wake_model_total="$(awk -v a="$wake_model_total" -v d="$gen_dt" 'BEGIN { printf "%.1f", a + d }')"
501
+ clast_porcelain_info " done in ${gen_dt}s (model total ${wake_model_total}s)"
438
502
 
439
503
  local choice
440
- choice="$(_clast_wake_prompt_choice)"
504
+ if (( auto )); then
505
+ # Guard unattended writes: skip a draft whose body is below the minimum
506
+ # length (likely a trivial session). Skipped — not dismissed — so it
507
+ # stays uncurated and can be handled in a later interactive pass.
508
+ local auto_body auto_len
509
+ auto_body="$(_clast_wake_strip_tags_trailer "$draft")"
510
+ auto_body="${auto_body#"${auto_body%%[![:space:]]*}"}" # ltrim
511
+ auto_body="${auto_body%"${auto_body##*[![:space:]]}"}" # rtrim
512
+ auto_len=${#auto_body}
513
+ if (( auto_min_chars > 0 && auto_len < auto_min_chars )); then
514
+ clast_porcelain_info " Draft below threshold (${auto_len} < ${auto_min_chars} chars) — skipping (stays uncurated)."
515
+ skipped_count=$(( skipped_count + 1 ))
516
+ drafting=0
517
+ continue
518
+ fi
519
+ # Auto-accept without printing the full draft or prompting — the write
520
+ # result below records what landed. Reuses the `a` case verbatim.
521
+ choice="a"
522
+ else
523
+ printf '\n%s\n' "$draft"
524
+ printf '\n'
525
+ choice="$(_clast_wake_prompt_choice)"
526
+ fi
441
527
 
442
528
  case "$choice" in
443
529
  a|A)
@@ -511,4 +597,5 @@ Revisions requested by user: ${edit_extra}"
511
597
  if (( remaining > 0 )); then
512
598
  clast_porcelain_info " Remaining: $remaining session(s) (stopped early)"
513
599
  fi
600
+ clast_porcelain_info " Model time: ${wake_model_total}s"
514
601
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@procrastivity/clast",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Capture, curate, and surface Claude Code session history across all your projects.",
5
5
  "homepage": "https://github.com/procrastivity/clast#readme",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: wake
3
- description: 'Generate curated journal entries from yesterday''s Claude Code sessions across all projects. Use when the user says "/wake", "wake", "morning briefing", "catch me up on yesterday", "what did I work on yesterday", "review my day", "process yesterday''s sessions", or otherwise signals they want to curate prior work across projects at the start of a new day. Runs `clast-plumbing snapshot` to ensure fresh data, then walks through each uncurated session from yesterday and proposes a draft entry the user can accept, edit, or skip. Prompts for promotion of decisions, common-issues, and workflows per accepted session. This is the once-per-day curation flow; for per-project briefings use /brief; for mid-session pivots use session-brief.'
3
+ description: 'Generate curated journal entries from yesterday''s Claude Code sessions across all projects. Use when the user says "/wake", "wake", "morning briefing", "catch me up on yesterday", "what did I work on yesterday", "review my day", "process yesterday''s sessions", or otherwise signals they want to curate prior work across projects at the start of a new day. Runs `clast-plumbing snapshot` to ensure fresh data, then walks through each uncurated session from yesterday and proposes a draft entry the user can accept, edit, or skip. Prompts for promotion of decisions, common-issues, and workflows per accepted session. Supports an opt-in auto mode when the user explicitly asks to curate everything without review ("do them all", "don''t ask me") — the equivalent of `clast wake --auto`. This is the once-per-day curation flow; for per-project briefings use /brief; for mid-session pivots use session-brief.'
4
4
  ---
5
5
 
6
6
  # Wake
@@ -81,7 +81,7 @@ print "Nothing to curate — all remaining sessions were empty or slash-command-
81
81
 
82
82
  ### Triage when multiple days have uncurated sessions
83
83
 
84
- If uncurated sessions span more than one day (e.g., after a weekend or break), present a triage step before processing. Show a per-day breakdown:
84
+ If uncurated sessions span more than one day (e.g., after a weekend or break), present a triage step before processing. (In [Auto mode](#auto-mode) skip triage entirely and process the whole window.) Show a per-day breakdown:
85
85
 
86
86
  ```
87
87
  Found 45 uncurated sessions across 5 days (2026-05-26 to 2026-05-30).
@@ -128,7 +128,7 @@ For each session in the list:
128
128
 
129
129
  4. Display the draft to the user inside a fenced markdown code block, with a brief preamble: "Here's a draft for the X session in <project> at HH:MM:".
130
130
 
131
- 5. Present the **promotion question** (see below) via AskUserQuestion.
131
+ 5. Present the **promotion question** (see below) via AskUserQuestion. (In [Auto mode](#auto-mode) skip this and accept the draft, subject to the length guard.)
132
132
 
133
133
  6. Handle the response:
134
134
  - **Accept** (any combination of accept-flavored options): pipe the draft to `clast-plumbing entries write` via stdin.
@@ -147,6 +147,8 @@ Auto-dismissed (no-op): 5 sessions.
147
147
  Skipped: 1 session.
148
148
  Remaining uncurated: 0.
149
149
 
150
+ (In Auto mode, also report: "Skipped (below length threshold): N sessions.")
151
+
150
152
  Promoted:
151
153
  Decisions: 1
152
154
  Common-issues: 0
@@ -155,6 +157,35 @@ Promoted:
155
157
  Run `/brief <project>` to start working on a specific project today.
156
158
  ```
157
159
 
160
+ ## Auto mode
161
+
162
+ By default every session gets its own AskUserQuestion — the friction is the point, it's where
163
+ curation happens. But if the user **explicitly** asks to curate everything without reviewing it
164
+ ("do them all", "don't ask me", "no prompts", "just write them all"), switch to auto mode.
165
+
166
+ This mirrors `clast wake --auto` in the CLI porcelain, and behaves the same way:
167
+
168
+ - **Skip the triage step** (Step 2) even when the backlog spans multiple days. The whole scan
169
+ window is processed.
170
+ - **Skip the per-session promotion question** (Step 3, item 5). Accept and write every draft via
171
+ the normal `entries write` path in "Writing the entry" below. Nothing is promoted — promotion
172
+ requires a human choice.
173
+ - **Length guard.** Before writing, take the draft body *without* the suggested-tags trailer and
174
+ trim surrounding whitespace. If it is shorter than `CLAST_WAKE_AUTO_MIN_CHARS` characters
175
+ (default `60`; `0` disables the guard), **skip** it — do not write it, and do **not** dismiss
176
+ it. It stays uncurated so it can be handled in a later interactive pass. Report it as
177
+ `Draft below threshold (N < M chars) — skipping (stays uncurated).`
178
+ - **A draft that fails to generate is skipped**, not retried — there's no reviewer to ask.
179
+ - Step 2a no-op auto-dismissal still runs first, unchanged.
180
+ - Don't print each draft in full; the write confirmation is the record of what landed.
181
+
182
+ Everything else is unchanged. Announce the mode up front ("Auto mode: drafts will be accepted
183
+ without review.") and report the counts in the Step 4 summary, including how many were skipped
184
+ for being under the length threshold.
185
+
186
+ For unattended/cron curation outside a Claude Code session, the CLI equivalent is
187
+ `clast wake --auto`.
188
+
158
189
  ## Draft generation prompt
159
190
 
160
191
  The prompt templates are installed alongside the plugin under `$CLAUDE_PLUGIN_ROOT/lib/clast/prompts/`:
@@ -225,4 +256,4 @@ For promoted items (decisions, common-issues, workflows): currently these are tr
225
256
  - **Multi-day backlog**: present the triage step (Step 2) so the user can choose scope before processing.
226
257
  - **`clast-plumbing snapshot` fails**: warn the user, then attempt to proceed with whatever's already in the manifest.
227
258
  - **`clast-plumbing show` fails for a specific session**: skip that session, note it in the final summary, continue with the rest.
228
- - **User says "do them all without prompting"**: not a v1 feature. Each session gets its own AskUserQuestion. The friction is intentional it's where curation happens.
259
+ - **User says "do them all without prompting"**: switch to [Auto mode](#auto-mode) accept and write every draft, skipping triage and the per-session question, with the `CLAST_WAKE_AUTO_MIN_CHARS` length guard applied. This is opt-in only: the interactive per-session AskUserQuestion stays the default, because the friction is where curation happens.