@procrastivity/clast 0.0.6 → 0.0.7
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.
|
@@ -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="$(
|
|
211
|
+
t0="$(clast_porcelain_now)"
|
|
227
212
|
summary="$(_clast_retrosum_summary "$project" "$work_day" "$cache_id" "$body" "$cache_dir" "$refresh")" || rc=$?
|
|
228
|
-
t1="$(
|
|
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="$(
|
|
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
|
-
# ---
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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
package/skills/wake/SKILL.md
CHANGED
|
@@ -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"**:
|
|
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.
|