@procrastivity/clast 0.0.3 → 0.0.5
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.
- package/README.md +41 -38
- package/bin/clast +31 -122
- package/bin/clast-plumbing +180 -0
- package/examples/cron/clast-snapshot.service +2 -2
- package/examples/cron/clast-snapshot.timer +1 -1
- package/examples/cron/crontab.sample +4 -4
- package/examples/workflows/morning-briefing.md +17 -17
- package/hooks/snapshot.sh +5 -5
- package/lib/clast/clast-classify-lib.bash +68 -0
- package/lib/clast/clast-dismissed-lib.bash +76 -5
- package/lib/clast/clast-lib.bash +93 -8
- package/lib/clast/clast-manifest-lib.bash +58 -5
- package/lib/clast/clast-porcelain-lib.bash +230 -0
- package/lib/clast/clast-porcelain-subcommands/brief.bash +236 -0
- package/lib/clast/clast-porcelain-subcommands/retro.bash +236 -0
- package/lib/clast/clast-porcelain-subcommands/undismiss.bash +27 -0
- package/lib/clast/clast-porcelain-subcommands/wake.bash +514 -0
- package/lib/clast/clast-registry-lib.bash +164 -41
- package/lib/clast/clast-retro-lib.bash +397 -0
- package/lib/clast/clast-subcommands/doctor.bash +29 -13
- package/lib/clast/clast-subcommands/entries.bash +40 -50
- package/lib/clast/clast-subcommands/projects.bash +16 -20
- package/lib/clast/clast-subcommands/registry.bash +26 -11
- package/lib/clast/clast-subcommands/retro.bash +217 -0
- package/lib/clast/clast-subcommands/sessions.bash +193 -48
- package/lib/clast/clast-subcommands/show.bash +69 -12
- package/lib/clast/clast-subcommands/snapshot.bash +29 -11
- package/lib/clast/clast-subcommands/stats.bash +7 -2
- package/lib/clast/prompts/brief-system.md +11 -5
- package/lib/clast/prompts/brief-user.md +4 -1
- package/lib/clast/prompts/retro-summary-system.md +14 -0
- package/lib/clast/prompts/retro-summary-user.md +7 -0
- package/lib/clast/prompts/{day-wakeup-draft-system.md → wake-draft-system.md} +1 -1
- package/package.json +3 -3
- package/{.claude-plugin/skills/wakeup → skills/brief}/SKILL.md +22 -20
- package/{.claude-plugin/skills/day-wakeup → skills/wake}/SKILL.md +54 -29
- package/bin/clast-brief +0 -305
- package/bin/clast-wake +0 -579
- /package/lib/clast/prompts/{day-wakeup-draft-user.md → wake-draft-user.md} +0 -0
package/bin/clast-wake
DELETED
|
@@ -1,579 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# clast-wake — LLM-powered day curation without Claude Code.
|
|
3
|
-
#
|
|
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.
|
|
7
|
-
#
|
|
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
|
|
13
|
-
|
|
14
|
-
CLAST_WAKE_DIR="$(cd "$(dirname "$(realpath "$0")")/.." && pwd)"
|
|
15
|
-
|
|
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() {
|
|
23
|
-
local s="$1"
|
|
24
|
-
s="${s,,}"
|
|
25
|
-
s="$(printf '%s' "$s" | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//')"
|
|
26
|
-
printf '%s' "${s:0:60}"
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
separator() {
|
|
30
|
-
local label="$1"
|
|
31
|
-
local width=72
|
|
32
|
-
local pad=$(( width - ${#label} - 4 ))
|
|
33
|
-
(( pad < 2 )) && pad=2
|
|
34
|
-
local dashes=""
|
|
35
|
-
local j
|
|
36
|
-
for (( j = 0; j < pad; j++ )); do dashes+="─"; done
|
|
37
|
-
printf '── %s %s\n' "$label" "$dashes"
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
# ── Preflight ────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
preflight() {
|
|
43
|
-
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
|
|
144
|
-
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)"
|
|
153
|
-
}
|
|
154
|
-
|
|
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
|
-
}
|
|
164
|
-
|
|
165
|
-
build_user_prompt() {
|
|
166
|
-
local project="$1" branch="$2" start="$3" end="$4" msg_count="$5"
|
|
167
|
-
local first_turns="$6" last_turns="$7" breadcrumbs="$8"
|
|
168
|
-
|
|
169
|
-
local prompt_dir
|
|
170
|
-
prompt_dir="$(resolve_prompt_dir)"
|
|
171
|
-
local template_file="$prompt_dir/day-wakeup-draft-user.md"
|
|
172
|
-
|
|
173
|
-
if [[ -r "$template_file" ]]; then
|
|
174
|
-
local template
|
|
175
|
-
template="$(cat "$template_file")"
|
|
176
|
-
template="${template//\{\{project\}\}/${project}}"
|
|
177
|
-
template="${template//\{\{branch\}\}/${branch:-unknown}}"
|
|
178
|
-
template="${template//\{\{start\}\}/${start}}"
|
|
179
|
-
template="${template//\{\{end\}\}/${end}}"
|
|
180
|
-
template="${template//\{\{msg_count\}\}/${msg_count}}"
|
|
181
|
-
template="${template//\{\{first_turns\}\}/${first_turns}}"
|
|
182
|
-
template="${template//\{\{last_turns\}\}/${last_turns}}"
|
|
183
|
-
template="${template//\{\{breadcrumbs\}\}/${breadcrumbs:-None.}}"
|
|
184
|
-
printf '%s' "$template"
|
|
185
|
-
else
|
|
186
|
-
warn "user prompt template not found: $template_file — using inline fallback"
|
|
187
|
-
cat <<EOF
|
|
188
|
-
Session metadata:
|
|
189
|
-
- Project: ${project}
|
|
190
|
-
- Branch: ${branch:-unknown}
|
|
191
|
-
- Start: ${start}
|
|
192
|
-
- End: ${end}
|
|
193
|
-
- Approximate messages: ${msg_count}
|
|
194
|
-
|
|
195
|
-
First turns of the session:
|
|
196
|
-
${first_turns}
|
|
197
|
-
|
|
198
|
-
Last turns of the session:
|
|
199
|
-
${last_turns}
|
|
200
|
-
|
|
201
|
-
Breadcrumbs the user left during this session's day:
|
|
202
|
-
${breadcrumbs:-None.}
|
|
203
|
-
EOF
|
|
204
|
-
fi
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
# ── Draft parsing ────────────────────────────────────────────────────
|
|
208
|
-
|
|
209
|
-
extract_title() {
|
|
210
|
-
local draft="$1"
|
|
211
|
-
local title
|
|
212
|
-
title="$(grep -m1 '^# Session:' <<<"$draft" | sed 's/^# Session:[[:space:]]*//')"
|
|
213
|
-
printf '%s' "$title"
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
extract_tags() {
|
|
217
|
-
local draft="$1"
|
|
218
|
-
local tags
|
|
219
|
-
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"
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
strip_tags_trailer() {
|
|
225
|
-
local draft="$1"
|
|
226
|
-
printf '%s' "$draft" | sed '/^$/{ N; /\n[Ss]uggested tags:/d; }' | sed '/^[Ss]uggested tags:/d'
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
# ── Session slug ─────────────────────────────────────────────────────
|
|
230
|
-
|
|
231
|
-
get_session_slug() {
|
|
232
|
-
local snapshot_path="$1" draft_title="$2"
|
|
233
|
-
local journal_dir
|
|
234
|
-
journal_dir="$(clast whereami 2>/dev/null | grep '^journal_dir:' | awk '{print $2}')" || true
|
|
235
|
-
if [[ -z "$journal_dir" ]]; then
|
|
236
|
-
journal_dir="${HOME}/.claude/journal"
|
|
237
|
-
fi
|
|
238
|
-
|
|
239
|
-
local abs_path="$journal_dir/$snapshot_path"
|
|
240
|
-
|
|
241
|
-
if [[ -r "$abs_path" ]]; then
|
|
242
|
-
local ai_title
|
|
243
|
-
ai_title="$(jq -r 'select(.type == "ai-title") | .aiTitle' "$abs_path" 2>/dev/null | tail -1)" || true
|
|
244
|
-
if [[ -n "$ai_title" && "$ai_title" != "null" ]]; then
|
|
245
|
-
slugify "$ai_title"
|
|
246
|
-
return
|
|
247
|
-
fi
|
|
248
|
-
fi
|
|
249
|
-
|
|
250
|
-
if [[ -n "$draft_title" ]]; then
|
|
251
|
-
slugify "$draft_title"
|
|
252
|
-
return
|
|
253
|
-
fi
|
|
254
|
-
|
|
255
|
-
printf 'session'
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
# ── Interactive menu ─────────────────────────────────────────────────
|
|
259
|
-
|
|
260
|
-
prompt_choice() {
|
|
261
|
-
local choice
|
|
262
|
-
printf '\n [a] Accept [e] Edit [d] Dismiss [s] Skip [q] Stop here\n' >/dev/tty
|
|
263
|
-
printf ' Choice: ' >/dev/tty
|
|
264
|
-
read -r -n1 choice </dev/tty
|
|
265
|
-
printf '\n' >/dev/tty
|
|
266
|
-
printf '%s' "$choice"
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
prompt_edit_feedback() {
|
|
270
|
-
local feedback
|
|
271
|
-
printf '\n What should change? ' >/dev/tty
|
|
272
|
-
read -r feedback </dev/tty
|
|
273
|
-
printf '%s' "$feedback"
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
# ── Triage ───────────────────────────────────────────────────────────
|
|
277
|
-
|
|
278
|
-
_clast_wake_triage() {
|
|
279
|
-
local uncurated="$1" total="$2" day_count="$3"
|
|
280
|
-
local first_day="$4" last_day="$5" project_count="$6"
|
|
281
|
-
|
|
282
|
-
info "Found $total uncurated session(s) across $day_count day(s) ($first_day to $last_day)." >/dev/tty
|
|
283
|
-
printf '\n' >/dev/tty
|
|
284
|
-
|
|
285
|
-
# Show per-day breakdown
|
|
286
|
-
local breakdown
|
|
287
|
-
breakdown="$(jq -r '
|
|
288
|
-
group_by(.day_bucket) | sort_by(.[0].day_bucket) | .[] |
|
|
289
|
-
" \(.[0].day_bucket) \(length) session(s)"
|
|
290
|
-
' <<<"$uncurated")"
|
|
291
|
-
printf '%s\n' "$breakdown" >/dev/tty
|
|
292
|
-
printf '\n' >/dev/tty
|
|
293
|
-
|
|
294
|
-
local choice
|
|
295
|
-
printf ' [a] Process all %s sessions\n' "$total" >/dev/tty
|
|
296
|
-
printf ' [y] Process yesterday only\n' >/dev/tty
|
|
297
|
-
printf ' [n] Choose how many days back\n' >/dev/tty
|
|
298
|
-
printf ' [o] Dismiss everything older, then process the rest\n' >/dev/tty
|
|
299
|
-
printf ' [q] Quit\n' >/dev/tty
|
|
300
|
-
printf ' Choice: ' >/dev/tty
|
|
301
|
-
read -r -n1 choice </dev/tty
|
|
302
|
-
printf '\n' >/dev/tty
|
|
303
|
-
|
|
304
|
-
case "$choice" in
|
|
305
|
-
a|A)
|
|
306
|
-
printf '%s' "$uncurated"
|
|
307
|
-
;;
|
|
308
|
-
y|Y)
|
|
309
|
-
local yesterday
|
|
310
|
-
yesterday="$(date -d 'yesterday' +%Y-%m-%d)"
|
|
311
|
-
jq -c "[.[] | select(.day_bucket == \"$yesterday\")]" <<<"$uncurated"
|
|
312
|
-
;;
|
|
313
|
-
n|N)
|
|
314
|
-
local days
|
|
315
|
-
printf ' How many days back? ' >/dev/tty
|
|
316
|
-
read -r days </dev/tty
|
|
317
|
-
if ! [[ "$days" =~ ^[1-9][0-9]*$ ]]; then
|
|
318
|
-
warn "invalid number — processing all sessions"
|
|
319
|
-
printf '%s' "$uncurated"
|
|
320
|
-
return
|
|
321
|
-
fi
|
|
322
|
-
local cutoff
|
|
323
|
-
cutoff="$(date -d "-${days} days" +%Y-%m-%d 2>/dev/null)" || {
|
|
324
|
-
warn "failed to compute date — processing all sessions"
|
|
325
|
-
printf '%s' "$uncurated"
|
|
326
|
-
return
|
|
327
|
-
}
|
|
328
|
-
jq -c "[.[] | select(.day_bucket >= \"$cutoff\")]" <<<"$uncurated"
|
|
329
|
-
;;
|
|
330
|
-
o|O)
|
|
331
|
-
local days_keep
|
|
332
|
-
printf ' Keep how many days? (dismiss everything older) ' >/dev/tty
|
|
333
|
-
read -r days_keep </dev/tty
|
|
334
|
-
if ! [[ "$days_keep" =~ ^[1-9][0-9]*$ ]]; then
|
|
335
|
-
warn "invalid number — processing all sessions"
|
|
336
|
-
printf '%s' "$uncurated"
|
|
337
|
-
return
|
|
338
|
-
fi
|
|
339
|
-
local cutoff_keep
|
|
340
|
-
cutoff_keep="$(date -d "-${days_keep} days" +%Y-%m-%d 2>/dev/null)" || {
|
|
341
|
-
warn "failed to compute date — processing all sessions"
|
|
342
|
-
printf '%s' "$uncurated"
|
|
343
|
-
return
|
|
344
|
-
}
|
|
345
|
-
# Dismiss older sessions
|
|
346
|
-
local old_ids
|
|
347
|
-
old_ids="$(jq -r "[.[] | select(.day_bucket < \"$cutoff_keep\")] | .[].session_id" <<<"$uncurated")"
|
|
348
|
-
local dismiss_count=0
|
|
349
|
-
local old_id
|
|
350
|
-
while IFS= read -r old_id; do
|
|
351
|
-
[[ -z "$old_id" ]] && continue
|
|
352
|
-
clast sessions dismiss "$old_id" --reason "bulk dismiss via clast-wake triage" 2>/dev/null
|
|
353
|
-
dismiss_count=$(( dismiss_count + 1 ))
|
|
354
|
-
done <<<"$old_ids"
|
|
355
|
-
if (( dismiss_count > 0 )); then
|
|
356
|
-
info " Dismissed $dismiss_count older session(s)." >/dev/tty
|
|
357
|
-
fi
|
|
358
|
-
jq -c "[.[] | select(.day_bucket >= \"$cutoff_keep\")]" <<<"$uncurated"
|
|
359
|
-
;;
|
|
360
|
-
q|Q)
|
|
361
|
-
printf '[]'
|
|
362
|
-
;;
|
|
363
|
-
*)
|
|
364
|
-
warn "unknown choice — processing all sessions"
|
|
365
|
-
printf '%s' "$uncurated"
|
|
366
|
-
;;
|
|
367
|
-
esac
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
# ── Main ─────────────────────────────────────────────────────────────
|
|
371
|
-
|
|
372
|
-
main() {
|
|
373
|
-
preflight
|
|
374
|
-
|
|
375
|
-
info "Snapshotting fresh transcripts..."
|
|
376
|
-
if ! clast snapshot 2>/dev/null; then
|
|
377
|
-
warn "clast snapshot failed — proceeding with existing data"
|
|
378
|
-
fi
|
|
379
|
-
|
|
380
|
-
info "Checking for uncurated or stale sessions..."
|
|
381
|
-
local sessions_json
|
|
382
|
-
sessions_json="$(clast --json sessions --since -30d 2>/dev/null)" || {
|
|
383
|
-
die "failed to list sessions"
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
local uncurated
|
|
387
|
-
uncurated="$(jq -c '[.[] | select(.curated == false or .stale == true)]' <<<"$sessions_json")"
|
|
388
|
-
local total
|
|
389
|
-
total="$(jq 'length' <<<"$uncurated")"
|
|
390
|
-
|
|
391
|
-
if (( total == 0 )); then
|
|
392
|
-
info "Nothing to curate — all sessions are curated or dismissed."
|
|
393
|
-
exit 0
|
|
394
|
-
fi
|
|
395
|
-
|
|
396
|
-
local day_count first_day last_day project_count
|
|
397
|
-
day_count="$(jq '[.[].day_bucket] | unique | length' <<<"$uncurated")"
|
|
398
|
-
first_day="$(jq -r '[.[].day_bucket] | sort | first' <<<"$uncurated")"
|
|
399
|
-
last_day="$(jq -r '[.[].day_bucket] | sort | last' <<<"$uncurated")"
|
|
400
|
-
project_count="$(jq '[.[].project] | unique | length' <<<"$uncurated")"
|
|
401
|
-
|
|
402
|
-
if (( day_count > 1 )); then
|
|
403
|
-
uncurated="$(_clast_wake_triage "$uncurated" "$total" "$day_count" "$first_day" "$last_day" "$project_count")"
|
|
404
|
-
total="$(jq 'length' <<<"$uncurated")"
|
|
405
|
-
if (( total == 0 )); then
|
|
406
|
-
info "Nothing left to curate."
|
|
407
|
-
exit 0
|
|
408
|
-
fi
|
|
409
|
-
project_count="$(jq '[.[].project] | unique | length' <<<"$uncurated")"
|
|
410
|
-
fi
|
|
411
|
-
|
|
412
|
-
info "Processing $total session(s) across $project_count project(s)."
|
|
413
|
-
printf '\n'
|
|
414
|
-
|
|
415
|
-
local curated_count=0 skipped_count=0 dismissed_count=0
|
|
416
|
-
local -a curated_projects=()
|
|
417
|
-
local i=0 stop=0
|
|
418
|
-
|
|
419
|
-
while (( i < total && stop == 0 )); do
|
|
420
|
-
local session
|
|
421
|
-
session="$(jq -c ".[$i]" <<<"$uncurated")"
|
|
422
|
-
|
|
423
|
-
local sid project branch start_ts end_ts msg_count snapshot_path
|
|
424
|
-
sid="$(jq -r '.session_id' <<<"$session")"
|
|
425
|
-
project="$(jq -r '.project' <<<"$session")"
|
|
426
|
-
branch="$(jq -r '.branch // ""' <<<"$session")"
|
|
427
|
-
start_ts="$(jq -r '.start' <<<"$session")"
|
|
428
|
-
end_ts="$(jq -r '.end' <<<"$session")"
|
|
429
|
-
msg_count="$(jq -r '.msg_count_approx' <<<"$session")"
|
|
430
|
-
snapshot_path="$(jq -r '.snapshot_path' <<<"$session")"
|
|
431
|
-
|
|
432
|
-
local start_short="${start_ts:11:5}"
|
|
433
|
-
[[ -z "$start_short" ]] && start_short="${start_ts:0:10}"
|
|
434
|
-
|
|
435
|
-
local is_stale
|
|
436
|
-
is_stale="$(jq -r '.stale // false' <<<"$session")"
|
|
437
|
-
local label="Session $((i+1))/$total: $project"
|
|
438
|
-
[[ "$is_stale" == "true" ]] && label="$label [STALE]"
|
|
439
|
-
[[ -n "$start_short" ]] && label="$label ($start_short"
|
|
440
|
-
[[ -n "$branch" && "$branch" != "null" ]] && label="$label, $branch"
|
|
441
|
-
label="$label)"
|
|
442
|
-
|
|
443
|
-
separator "$label"
|
|
444
|
-
info "Gathering context..."
|
|
445
|
-
|
|
446
|
-
local show_json
|
|
447
|
-
show_json="$(clast --json show "$sid" --full --turns 8 2>/dev/null)" || {
|
|
448
|
-
warn "failed to read session $sid — skipping"
|
|
449
|
-
skipped_count=$(( skipped_count + 1 ))
|
|
450
|
-
i=$(( i + 1 ))
|
|
451
|
-
continue
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
local first_turns last_turns
|
|
455
|
-
first_turns="$(jq -r '
|
|
456
|
-
.first_turns // [] | .[] |
|
|
457
|
-
"[\(.role)] \(.text)"
|
|
458
|
-
' <<<"$show_json" 2>/dev/null)" || true
|
|
459
|
-
|
|
460
|
-
last_turns="$(jq -r '
|
|
461
|
-
.last_turns // [] | .[] |
|
|
462
|
-
"[\(.role)] \(.text)"
|
|
463
|
-
' <<<"$show_json" 2>/dev/null)" || true
|
|
464
|
-
|
|
465
|
-
local breadcrumbs=""
|
|
466
|
-
breadcrumbs="$(clast breadcrumb --read --project "$project" --day yesterday 2>/dev/null)" || true
|
|
467
|
-
|
|
468
|
-
local user_prompt
|
|
469
|
-
user_prompt="$(build_user_prompt "$project" "$branch" "$start_ts" "$end_ts" "$msg_count" \
|
|
470
|
-
"$first_turns" "$last_turns" "$breadcrumbs")"
|
|
471
|
-
|
|
472
|
-
local system_prompt
|
|
473
|
-
system_prompt="$(load_system_prompt)"
|
|
474
|
-
|
|
475
|
-
local draft="" edit_extra=""
|
|
476
|
-
local drafting=1
|
|
477
|
-
|
|
478
|
-
while (( drafting )); do
|
|
479
|
-
local full_system="$system_prompt"
|
|
480
|
-
local full_user="$user_prompt"
|
|
481
|
-
if [[ -n "$edit_extra" ]]; then
|
|
482
|
-
full_user="${full_user}
|
|
483
|
-
|
|
484
|
-
Revisions requested by user: ${edit_extra}"
|
|
485
|
-
fi
|
|
486
|
-
|
|
487
|
-
info "Generating draft..."
|
|
488
|
-
if ! draft="$(llm_chat "$full_system" "$full_user")"; then
|
|
489
|
-
printf '\n'
|
|
490
|
-
warn "LLM call failed for session $sid"
|
|
491
|
-
local retry
|
|
492
|
-
printf ' [r] Retry [s] Skip [q] Stop\n Choice: '
|
|
493
|
-
read -r -n1 retry </dev/tty
|
|
494
|
-
printf '\n'
|
|
495
|
-
case "$retry" in
|
|
496
|
-
r|R) continue ;;
|
|
497
|
-
q|Q) stop=1; break ;;
|
|
498
|
-
*) skipped_count=$(( skipped_count + 1 )); break ;;
|
|
499
|
-
esac
|
|
500
|
-
fi
|
|
501
|
-
|
|
502
|
-
printf '\n%s\n' "$draft"
|
|
503
|
-
printf '\n'
|
|
504
|
-
|
|
505
|
-
local choice
|
|
506
|
-
choice="$(prompt_choice)"
|
|
507
|
-
|
|
508
|
-
case "$choice" in
|
|
509
|
-
a|A)
|
|
510
|
-
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")"
|
|
515
|
-
|
|
516
|
-
local write_args=(entries write --session "$sid" --slug "$slug" --body-stdin)
|
|
517
|
-
[[ -n "$tags" ]] && write_args+=(--tags "$tags")
|
|
518
|
-
[[ -n "$title" ]] && write_args+=(--title "$title")
|
|
519
|
-
|
|
520
|
-
local write_result
|
|
521
|
-
if write_result="$(printf '%s\n' "$body" | clast "${write_args[@]}" 2>&1)"; then
|
|
522
|
-
info " $write_result"
|
|
523
|
-
curated_count=$(( curated_count + 1 ))
|
|
524
|
-
curated_projects+=("$project")
|
|
525
|
-
else
|
|
526
|
-
warn "failed to write entry: $write_result"
|
|
527
|
-
fi
|
|
528
|
-
drafting=0
|
|
529
|
-
;;
|
|
530
|
-
e|E)
|
|
531
|
-
edit_extra="$(prompt_edit_feedback)"
|
|
532
|
-
;;
|
|
533
|
-
d|D)
|
|
534
|
-
if clast sessions dismiss "$sid" --reason "dismissed via clast-wake" 2>/dev/null; then
|
|
535
|
-
info " Dismissed."
|
|
536
|
-
dismissed_count=$(( dismissed_count + 1 ))
|
|
537
|
-
else
|
|
538
|
-
warn "failed to dismiss session $sid"
|
|
539
|
-
fi
|
|
540
|
-
drafting=0
|
|
541
|
-
;;
|
|
542
|
-
s|S)
|
|
543
|
-
skipped_count=$(( skipped_count + 1 ))
|
|
544
|
-
drafting=0
|
|
545
|
-
;;
|
|
546
|
-
q|Q)
|
|
547
|
-
stop=1
|
|
548
|
-
drafting=0
|
|
549
|
-
;;
|
|
550
|
-
*)
|
|
551
|
-
info " Unknown choice '$choice' — skipping."
|
|
552
|
-
skipped_count=$(( skipped_count + 1 ))
|
|
553
|
-
drafting=0
|
|
554
|
-
;;
|
|
555
|
-
esac
|
|
556
|
-
done
|
|
557
|
-
|
|
558
|
-
i=$(( i + 1 ))
|
|
559
|
-
printf '\n'
|
|
560
|
-
done
|
|
561
|
-
|
|
562
|
-
local remaining=$(( total - curated_count - skipped_count - dismissed_count ))
|
|
563
|
-
local unique_projects
|
|
564
|
-
unique_projects="$(printf '%s\n' "${curated_projects[@]}" 2>/dev/null | sort -u | wc -l | tr -d ' ')" || true
|
|
565
|
-
[[ -z "$unique_projects" || "$unique_projects" == "0" ]] && unique_projects=0
|
|
566
|
-
|
|
567
|
-
printf '\n'
|
|
568
|
-
separator "Summary"
|
|
569
|
-
info " Curated: $curated_count session(s) across $unique_projects project(s)"
|
|
570
|
-
if (( dismissed_count > 0 )); then
|
|
571
|
-
info " Dismissed: $dismissed_count session(s)"
|
|
572
|
-
fi
|
|
573
|
-
info " Skipped: $skipped_count session(s)"
|
|
574
|
-
if (( remaining > 0 )); then
|
|
575
|
-
info " Remaining: $remaining session(s) (stopped early)"
|
|
576
|
-
fi
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
main "$@"
|
|
File without changes
|