@procrastivity/clast 0.0.1
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/.claude-plugin/plugin.json +11 -0
- package/.claude-plugin/skills/day-wakeup/SKILL.md +194 -0
- package/.claude-plugin/skills/wakeup/SKILL.md +115 -0
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/bin/.gitkeep +0 -0
- package/bin/clast +172 -0
- package/examples/config/config.toml.sample +32 -0
- package/examples/cron/clast-snapshot.service +10 -0
- package/examples/cron/clast-snapshot.timer +15 -0
- package/examples/cron/crontab.sample +16 -0
- package/examples/workflows/morning-briefing.md +137 -0
- package/hooks/hooks.json +8 -0
- package/hooks/snapshot.sh +15 -0
- package/lib/clast/.gitkeep +0 -0
- package/lib/clast/clast-decode-lib.bash +184 -0
- package/lib/clast/clast-lib.bash +233 -0
- package/lib/clast/clast-manifest-lib.bash +232 -0
- package/lib/clast/clast-registry-lib.bash +252 -0
- package/lib/clast/clast-subcommands/.gitkeep +0 -0
- package/lib/clast/clast-subcommands/breadcrumb.bash +454 -0
- package/lib/clast/clast-subcommands/doctor.bash +607 -0
- package/lib/clast/clast-subcommands/entries.bash +697 -0
- package/lib/clast/clast-subcommands/projects.bash +309 -0
- package/lib/clast/clast-subcommands/registry.bash +207 -0
- package/lib/clast/clast-subcommands/sessions.bash +275 -0
- package/lib/clast/clast-subcommands/show.bash +283 -0
- package/lib/clast/clast-subcommands/snapshot.bash +294 -0
- package/lib/clast/clast-subcommands/stats.bash +354 -0
- package/lib/clast/clast-subcommands/whereami.bash +108 -0
- package/package.json +39 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# clast-subcommands/sessions.bash — `clast sessions`.
|
|
2
|
+
#
|
|
3
|
+
# Read-only view over .manifest.jsonl: list sessions in a date window,
|
|
4
|
+
# optionally filtered by registry slug. See
|
|
5
|
+
# docs/cli-contract.md#clast-sessions.
|
|
6
|
+
# shellcheck shell=bash
|
|
7
|
+
# shellcheck source=lib/clast/clast-lib.bash
|
|
8
|
+
# shellcheck source=lib/clast/clast-manifest-lib.bash
|
|
9
|
+
# shellcheck source=lib/clast/clast-registry-lib.bash
|
|
10
|
+
# shellcheck source=lib/clast/clast-decode-lib.bash
|
|
11
|
+
|
|
12
|
+
_clast_sessions_usage() {
|
|
13
|
+
cat <<'EOF'
|
|
14
|
+
Usage: clast sessions [--day DATE] [--since DATE] [--until DATE] [--project SLUG]
|
|
15
|
+
|
|
16
|
+
List sessions captured in a date window.
|
|
17
|
+
|
|
18
|
+
Flags:
|
|
19
|
+
--day DATE Single-day window (default: today). Mutually exclusive
|
|
20
|
+
with --since/--until.
|
|
21
|
+
--since DATE Start of range (inclusive).
|
|
22
|
+
--until DATE End of range (inclusive).
|
|
23
|
+
--project SLUG Filter to a single registry slug.
|
|
24
|
+
-h, --help Print this usage and exit.
|
|
25
|
+
|
|
26
|
+
DATE accepts ISO (YYYY-MM-DD), `today`, `yesterday`, `last-week`,
|
|
27
|
+
`-Nd`, or `-Nw`. See docs/cli-contract.md#date-parsing.
|
|
28
|
+
EOF
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_clast_sessions_err() {
|
|
32
|
+
local msg="$1"
|
|
33
|
+
if [[ -n "${CLAST_JSON:-}" ]]; then
|
|
34
|
+
jq -cn --arg m "$msg" '{error:$m, code:2}'
|
|
35
|
+
else
|
|
36
|
+
clast_log_error "sessions: $msg"
|
|
37
|
+
fi
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
clast_cmd_sessions() {
|
|
41
|
+
local day_filter="" since_date="" until_date=""
|
|
42
|
+
local project_filter=""
|
|
43
|
+
|
|
44
|
+
while [[ $# -gt 0 ]]; do
|
|
45
|
+
case "$1" in
|
|
46
|
+
--day)
|
|
47
|
+
if [[ $# -lt 2 ]]; then _clast_sessions_err "--day requires a value"; return 2; fi
|
|
48
|
+
if ! day_filter="$(clast_parse_date "$2" 2>/dev/null)"; then
|
|
49
|
+
_clast_sessions_err "invalid date '$2'"; return 2
|
|
50
|
+
fi
|
|
51
|
+
shift 2 ;;
|
|
52
|
+
--day=*)
|
|
53
|
+
if ! day_filter="$(clast_parse_date "${1#*=}" 2>/dev/null)"; then
|
|
54
|
+
_clast_sessions_err "invalid date '${1#*=}'"; return 2
|
|
55
|
+
fi
|
|
56
|
+
shift ;;
|
|
57
|
+
--since)
|
|
58
|
+
if [[ $# -lt 2 ]]; then _clast_sessions_err "--since requires a value"; return 2; fi
|
|
59
|
+
if ! since_date="$(clast_parse_date "$2" 2>/dev/null)"; then
|
|
60
|
+
_clast_sessions_err "invalid date '$2'"; return 2
|
|
61
|
+
fi
|
|
62
|
+
shift 2 ;;
|
|
63
|
+
--since=*)
|
|
64
|
+
if ! since_date="$(clast_parse_date "${1#*=}" 2>/dev/null)"; then
|
|
65
|
+
_clast_sessions_err "invalid date '${1#*=}'"; return 2
|
|
66
|
+
fi
|
|
67
|
+
shift ;;
|
|
68
|
+
--until)
|
|
69
|
+
if [[ $# -lt 2 ]]; then _clast_sessions_err "--until requires a value"; return 2; fi
|
|
70
|
+
if ! until_date="$(clast_parse_date "$2" 2>/dev/null)"; then
|
|
71
|
+
_clast_sessions_err "invalid date '$2'"; return 2
|
|
72
|
+
fi
|
|
73
|
+
shift 2 ;;
|
|
74
|
+
--until=*)
|
|
75
|
+
if ! until_date="$(clast_parse_date "${1#*=}" 2>/dev/null)"; then
|
|
76
|
+
_clast_sessions_err "invalid date '${1#*=}'"; return 2
|
|
77
|
+
fi
|
|
78
|
+
shift ;;
|
|
79
|
+
--project)
|
|
80
|
+
if [[ $# -lt 2 ]]; then _clast_sessions_err "--project requires a value"; return 2; fi
|
|
81
|
+
project_filter="$2"; shift 2 ;;
|
|
82
|
+
--project=*)
|
|
83
|
+
project_filter="${1#*=}"; shift ;;
|
|
84
|
+
-h|--help)
|
|
85
|
+
_clast_sessions_usage; return 0 ;;
|
|
86
|
+
--)
|
|
87
|
+
shift; break ;;
|
|
88
|
+
-*)
|
|
89
|
+
_clast_sessions_err "unknown flag '$1'"; return 2 ;;
|
|
90
|
+
*)
|
|
91
|
+
_clast_sessions_err "unexpected positional '$1'"; return 2 ;;
|
|
92
|
+
esac
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
if [[ -n "$day_filter" && ( -n "$since_date" || -n "$until_date" ) ]]; then
|
|
96
|
+
_clast_sessions_err "--day is mutually exclusive with --since/--until"
|
|
97
|
+
return 2
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
if [[ -z "$day_filter" && -z "$since_date" && -z "$until_date" ]]; then
|
|
101
|
+
day_filter="$(clast_today)"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# `--until` defaults to `today` per docs/cli-contract.md#clast-sessions
|
|
105
|
+
# when `--since` is supplied without an explicit upper bound.
|
|
106
|
+
if [[ -n "$since_date" && -z "$until_date" ]]; then
|
|
107
|
+
until_date="$(clast_today)"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
local filter
|
|
111
|
+
filter="$(_clast_sessions_window_filter "$day_filter" "$since_date" "$until_date")"
|
|
112
|
+
|
|
113
|
+
local journal_dir
|
|
114
|
+
journal_dir="$(clast_journal_dir)"
|
|
115
|
+
|
|
116
|
+
# Build the project segment whitelist if --project was passed.
|
|
117
|
+
declare -A allowed_segs=()
|
|
118
|
+
local have_project_filter=0
|
|
119
|
+
if [[ -n "$project_filter" ]]; then
|
|
120
|
+
have_project_filter=1
|
|
121
|
+
local p seg_enc
|
|
122
|
+
while IFS= read -r p; do
|
|
123
|
+
[[ -z "$p" ]] && continue
|
|
124
|
+
seg_enc="$(clast_encode_path "$p")"
|
|
125
|
+
allowed_segs["$seg_enc"]=1
|
|
126
|
+
done < <(clast_registry_list_json \
|
|
127
|
+
| jq -r --arg s "$project_filter" '.[] | select(.slug == $s) | .path')
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Most-recent manifest line per session_id within window.
|
|
131
|
+
declare -A latest_line=()
|
|
132
|
+
local line sid
|
|
133
|
+
while IFS= read -r line; do
|
|
134
|
+
[[ -z "$line" ]] && continue
|
|
135
|
+
sid="$(jq -r '.session_id' <<<"$line")"
|
|
136
|
+
[[ -z "$sid" || "$sid" == "null" ]] && continue
|
|
137
|
+
latest_line["$sid"]="$line"
|
|
138
|
+
done < <(clast_manifest_iterate "$filter")
|
|
139
|
+
|
|
140
|
+
local -a rows=()
|
|
141
|
+
local snapshot day_bucket mtime seg abs_path msgs
|
|
142
|
+
local first_ts last_ts start_ts end_ts curated branch slug
|
|
143
|
+
local entries_dir
|
|
144
|
+
entries_dir="$journal_dir/entries"
|
|
145
|
+
|
|
146
|
+
for sid in "${!latest_line[@]}"; do
|
|
147
|
+
line="${latest_line[$sid]}"
|
|
148
|
+
snapshot="$(jq -r '.snapshot' <<<"$line")"
|
|
149
|
+
day_bucket="$(jq -r '.day_bucket' <<<"$line")"
|
|
150
|
+
mtime="$(jq -r '.source_mtime' <<<"$line")"
|
|
151
|
+
seg="$(awk -F/ 'NR==1{print $3}' <<<"$snapshot")"
|
|
152
|
+
|
|
153
|
+
if (( have_project_filter == 1 )); then
|
|
154
|
+
if [[ -z "${allowed_segs[$seg]:-}" ]]; then
|
|
155
|
+
continue
|
|
156
|
+
fi
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
abs_path="$journal_dir/$snapshot"
|
|
160
|
+
if [[ -r "$abs_path" ]]; then
|
|
161
|
+
msgs="$(wc -l <"$abs_path" 2>/dev/null | tr -d ' ')"
|
|
162
|
+
[[ -z "$msgs" ]] && msgs=0
|
|
163
|
+
first_ts="$(head -n1 "$abs_path" 2>/dev/null | jq -r '.timestamp // empty' 2>/dev/null || true)"
|
|
164
|
+
last_ts="$(tail -n1 "$abs_path" 2>/dev/null | jq -r '.timestamp // empty' 2>/dev/null || true)"
|
|
165
|
+
else
|
|
166
|
+
if [[ -n "${CLAST_VERBOSE:-}" ]]; then
|
|
167
|
+
clast_log_warn "sessions: snapshot missing or unreadable: $abs_path"
|
|
168
|
+
fi
|
|
169
|
+
msgs=0
|
|
170
|
+
first_ts=""
|
|
171
|
+
last_ts=""
|
|
172
|
+
fi
|
|
173
|
+
start_ts="${first_ts:-$mtime}"
|
|
174
|
+
end_ts="${last_ts:-$mtime}"
|
|
175
|
+
|
|
176
|
+
if slug="$(clast_registry_resolve "$seg" 2>/dev/null)" && [[ -n "$slug" ]]; then
|
|
177
|
+
:
|
|
178
|
+
else
|
|
179
|
+
slug="$seg"
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# TODO(step-10): branch field is best-effort; revisit when stats command lands.
|
|
183
|
+
branch=""
|
|
184
|
+
|
|
185
|
+
curated=false
|
|
186
|
+
if [[ -d "$entries_dir" ]]; then
|
|
187
|
+
if grep -l "session_id: $sid" "$entries_dir"/*.md 2>/dev/null | head -n1 | grep -q .; then
|
|
188
|
+
curated=true
|
|
189
|
+
fi
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
rows+=("$(jq -cn \
|
|
193
|
+
--arg session_id "$sid" \
|
|
194
|
+
--arg project "$slug" \
|
|
195
|
+
--arg segment "$seg" \
|
|
196
|
+
--arg branch "$branch" \
|
|
197
|
+
--arg start "$start_ts" \
|
|
198
|
+
--arg end "$end_ts" \
|
|
199
|
+
--argjson msg_count_approx "$msgs" \
|
|
200
|
+
--arg snapshot_path "$snapshot" \
|
|
201
|
+
--arg day_bucket "$day_bucket" \
|
|
202
|
+
--argjson curated "$curated" \
|
|
203
|
+
'{
|
|
204
|
+
session_id: $session_id,
|
|
205
|
+
project: $project,
|
|
206
|
+
segment: $segment,
|
|
207
|
+
branch: (if $branch == "" then null else $branch end),
|
|
208
|
+
start: $start,
|
|
209
|
+
end: $end,
|
|
210
|
+
msg_count_approx: $msg_count_approx,
|
|
211
|
+
snapshot_path: $snapshot_path,
|
|
212
|
+
day_bucket: $day_bucket,
|
|
213
|
+
curated: $curated
|
|
214
|
+
}')")
|
|
215
|
+
done
|
|
216
|
+
|
|
217
|
+
local rows_json='[]'
|
|
218
|
+
if (( ${#rows[@]} > 0 )); then
|
|
219
|
+
rows_json="$(printf '%s\n' "${rows[@]}" | jq -cs 'sort_by(.start)')"
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
if [[ -n "${CLAST_JSON:-}" ]]; then
|
|
223
|
+
printf '%s\n' "$rows_json"
|
|
224
|
+
return 0
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
if [[ -n "${CLAST_QUIET:-}" ]]; then
|
|
228
|
+
return 0
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
printf '%-37s %-17s %-25s %5s %5s %s\n' \
|
|
232
|
+
"session_id" "project" "branch" "start" "end" "msgs"
|
|
233
|
+
|
|
234
|
+
local n i sj r_sid r_project r_branch r_start r_end r_msgs r_day disp_start disp_end
|
|
235
|
+
n="$(jq 'length' <<<"$rows_json")"
|
|
236
|
+
for (( i = 0; i < n; i++ )); do
|
|
237
|
+
sj="$(jq -c ".[$i]" <<<"$rows_json")"
|
|
238
|
+
r_sid="$(jq -r '.session_id' <<<"$sj")"
|
|
239
|
+
r_project="$(jq -r '.project' <<<"$sj")"
|
|
240
|
+
r_branch="$(jq -r '.branch // ""' <<<"$sj")"
|
|
241
|
+
r_start="$(jq -r '.start' <<<"$sj")"
|
|
242
|
+
r_end="$(jq -r '.end' <<<"$sj")"
|
|
243
|
+
r_msgs="$(jq -r '.msg_count_approx' <<<"$sj")"
|
|
244
|
+
r_day="$(jq -r '.day_bucket' <<<"$sj")"
|
|
245
|
+
|
|
246
|
+
if [[ "${r_start:0:10}" == "${r_end:0:10}" && "${r_start:0:10}" == "$r_day" ]]; then
|
|
247
|
+
disp_start="${r_start:11:5}"
|
|
248
|
+
disp_end="${r_end:11:5}"
|
|
249
|
+
else
|
|
250
|
+
disp_start="${r_start:0:10} ${r_start:11:5}"
|
|
251
|
+
disp_end="${r_end:0:10} ${r_end:11:5}"
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
printf '%-37s %-17s %-25s %5s %5s %s\n' \
|
|
255
|
+
"$r_sid" "$r_project" "$r_branch" "$disp_start" "$disp_end" "$r_msgs"
|
|
256
|
+
done
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# _clast_sessions_window_filter <day> <since> <until>
|
|
260
|
+
_clast_sessions_window_filter() {
|
|
261
|
+
local day="$1" since="$2" until="$3"
|
|
262
|
+
if [[ -n "$day" ]]; then
|
|
263
|
+
printf '.day_bucket == "%s"' "$day"
|
|
264
|
+
return 0
|
|
265
|
+
fi
|
|
266
|
+
local -a parts=()
|
|
267
|
+
if [[ -n "$since" ]]; then parts+=(".day_bucket >= \"$since\""); fi
|
|
268
|
+
if [[ -n "$until" ]]; then parts+=(".day_bucket <= \"$until\""); fi
|
|
269
|
+
if (( ${#parts[@]} == 0 )); then printf 'true'; return 0; fi
|
|
270
|
+
local joined="${parts[0]}" i
|
|
271
|
+
for (( i = 1; i < ${#parts[@]}; i++ )); do
|
|
272
|
+
joined+=" and ${parts[i]}"
|
|
273
|
+
done
|
|
274
|
+
printf '%s' "$joined"
|
|
275
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# clast-subcommands/show.bash — `clast show <session-id>`.
|
|
2
|
+
#
|
|
3
|
+
# Dump metadata + (optionally) first/last turns of a single captured
|
|
4
|
+
# session. See docs/cli-contract.md#clast-show.
|
|
5
|
+
# shellcheck shell=bash
|
|
6
|
+
# shellcheck source=lib/clast/clast-lib.bash
|
|
7
|
+
# shellcheck source=lib/clast/clast-manifest-lib.bash
|
|
8
|
+
# shellcheck source=lib/clast/clast-registry-lib.bash
|
|
9
|
+
# shellcheck source=lib/clast/clast-decode-lib.bash
|
|
10
|
+
|
|
11
|
+
_clast_show_usage() {
|
|
12
|
+
cat <<'EOF'
|
|
13
|
+
Usage: clast show <session-id> [--full] [--turns N]
|
|
14
|
+
|
|
15
|
+
Print metadata for a captured session.
|
|
16
|
+
|
|
17
|
+
Flags:
|
|
18
|
+
--full Include first/last N turns (text only, no tool calls).
|
|
19
|
+
--turns N Number of turns at each end when --full is set (default 5).
|
|
20
|
+
-h, --help Print this usage and exit.
|
|
21
|
+
EOF
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_clast_show_err() {
|
|
25
|
+
local msg="$1" code="${2:-2}"
|
|
26
|
+
if [[ -n "${CLAST_JSON:-}" ]]; then
|
|
27
|
+
jq -cn --arg m "$msg" --argjson c "$code" '{error:$m, code:$c}'
|
|
28
|
+
else
|
|
29
|
+
clast_log_error "show: $msg"
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
clast_cmd_show() {
|
|
34
|
+
local session_id=""
|
|
35
|
+
local include_turns=0
|
|
36
|
+
local turn_count=5
|
|
37
|
+
|
|
38
|
+
while [[ $# -gt 0 ]]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
--full) include_turns=1; shift ;;
|
|
41
|
+
--turns)
|
|
42
|
+
if [[ $# -lt 2 ]]; then _clast_show_err "--turns requires a value"; return 2; fi
|
|
43
|
+
if ! [[ "$2" =~ ^[1-9][0-9]*$ ]]; then
|
|
44
|
+
_clast_show_err "--turns must be a positive integer"; return 2
|
|
45
|
+
fi
|
|
46
|
+
turn_count="$2"; shift 2 ;;
|
|
47
|
+
--turns=*)
|
|
48
|
+
local v="${1#*=}"
|
|
49
|
+
if ! [[ "$v" =~ ^[1-9][0-9]*$ ]]; then
|
|
50
|
+
_clast_show_err "--turns must be a positive integer"; return 2
|
|
51
|
+
fi
|
|
52
|
+
turn_count="$v"; shift ;;
|
|
53
|
+
-h|--help) _clast_show_usage; return 0 ;;
|
|
54
|
+
--) shift; break ;;
|
|
55
|
+
-*) _clast_show_err "unknown flag '$1'"; return 2 ;;
|
|
56
|
+
*)
|
|
57
|
+
if [[ -n "$session_id" ]]; then
|
|
58
|
+
_clast_show_err "unexpected positional '$1'"; return 2
|
|
59
|
+
fi
|
|
60
|
+
session_id="$1"; shift ;;
|
|
61
|
+
esac
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
if [[ -z "$session_id" ]]; then
|
|
65
|
+
_clast_show_err "missing <session-id>"; return 2
|
|
66
|
+
fi
|
|
67
|
+
if ! [[ "$session_id" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
|
|
68
|
+
_clast_show_err "'$session_id' is not a valid UUID"; return 2
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
local line
|
|
72
|
+
if ! line="$(clast_manifest_lookup "$session_id" 2>/dev/null)" || [[ -z "$line" ]]; then
|
|
73
|
+
_clast_show_err "session '$session_id' not found in manifest" 1
|
|
74
|
+
return 1
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
local snapshot day_bucket source_mtime journal_dir abs_path
|
|
78
|
+
snapshot="$(jq -r '.snapshot' <<<"$line")"
|
|
79
|
+
day_bucket="$(jq -r '.day_bucket' <<<"$line")"
|
|
80
|
+
source_mtime="$(jq -r '.source_mtime' <<<"$line")"
|
|
81
|
+
journal_dir="$(clast_journal_dir)"
|
|
82
|
+
abs_path="$journal_dir/$snapshot"
|
|
83
|
+
|
|
84
|
+
if [[ ! -r "$abs_path" ]]; then
|
|
85
|
+
_clast_show_err "snapshot file missing on disk (run 'clast doctor')" 1
|
|
86
|
+
return 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
local seg slug
|
|
90
|
+
seg="$(awk -F/ 'NR==1{print $3}' <<<"$snapshot")"
|
|
91
|
+
if slug="$(clast_registry_resolve "$seg" 2>/dev/null)" && [[ -n "$slug" ]]; then
|
|
92
|
+
:
|
|
93
|
+
else
|
|
94
|
+
slug="$seg"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
local msgs first_ts last_ts start_ts end_ts
|
|
98
|
+
msgs="$(wc -l <"$abs_path" 2>/dev/null | tr -d ' ')"
|
|
99
|
+
[[ -z "$msgs" ]] && msgs=0
|
|
100
|
+
first_ts="$(head -n1 "$abs_path" 2>/dev/null | jq -r '.timestamp // empty' 2>/dev/null || true)"
|
|
101
|
+
last_ts="$(tail -n1 "$abs_path" 2>/dev/null | jq -r '.timestamp // empty' 2>/dev/null || true)"
|
|
102
|
+
start_ts="${first_ts:-$source_mtime}"
|
|
103
|
+
end_ts="${last_ts:-$source_mtime}"
|
|
104
|
+
|
|
105
|
+
# first_prompt / last_prompt — best-effort scan of user messages.
|
|
106
|
+
# These pipelines must never abort the command: malformed JSONL lines are
|
|
107
|
+
# routine, and the fields are explicitly documented as best-effort.
|
|
108
|
+
local first_prompt last_prompt user_msgs=""
|
|
109
|
+
user_msgs="$(_clast_show_user_messages "$abs_path" 2>/dev/null || true)"
|
|
110
|
+
first_prompt="$(printf '%s\n' "$user_msgs" | head -n1)"
|
|
111
|
+
last_prompt="$(printf '%s\n' "$user_msgs" | tail -n1)"
|
|
112
|
+
# If the stream was empty the head/tail of '\n' is empty — keep as ''.
|
|
113
|
+
[[ -z "$user_msgs" ]] && first_prompt="" && last_prompt=""
|
|
114
|
+
first_prompt="$(_clast_show_truncate "$first_prompt")"
|
|
115
|
+
last_prompt="$(_clast_show_truncate "$last_prompt")"
|
|
116
|
+
|
|
117
|
+
# duration
|
|
118
|
+
local duration_str=""
|
|
119
|
+
if [[ -n "$first_ts" && -n "$last_ts" ]]; then
|
|
120
|
+
local s_epoch e_epoch delta
|
|
121
|
+
if s_epoch="$(date -d "$first_ts" +%s 2>/dev/null)" \
|
|
122
|
+
&& e_epoch="$(date -d "$last_ts" +%s 2>/dev/null)" \
|
|
123
|
+
&& [[ -n "$s_epoch" && -n "$e_epoch" ]]; then
|
|
124
|
+
delta=$((e_epoch - s_epoch))
|
|
125
|
+
duration_str="$(_clast_show_format_duration "$delta")"
|
|
126
|
+
fi
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
local curated=false
|
|
130
|
+
if [[ -d "$journal_dir/entries" ]]; then
|
|
131
|
+
if grep -l "session_id: $session_id" "$journal_dir/entries"/*.md 2>/dev/null | head -n1 | grep -q .; then
|
|
132
|
+
curated=true
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# TODO(step-10): branch field is best-effort; revisit when stats command lands.
|
|
137
|
+
local branch=""
|
|
138
|
+
|
|
139
|
+
# --- Build first_turns / last_turns only when --full is set ------------
|
|
140
|
+
# Collecting turns slurps the entire JSONL into jq; skip the cost when
|
|
141
|
+
# --full wasn't requested (the JSON object never emits these arrays
|
|
142
|
+
# without --full).
|
|
143
|
+
local turns_json='[]' first_turns_json='[]' last_turns_json='[]'
|
|
144
|
+
if (( include_turns == 1 )); then
|
|
145
|
+
turns_json="$(_clast_show_collect_turns "$abs_path")"
|
|
146
|
+
first_turns_json="$(jq -c --argjson n "$turn_count" '.[0:$n]' <<<"$turns_json")"
|
|
147
|
+
last_turns_json="$(jq -c --argjson n "$turn_count" '.[-$n:]' <<<"$turns_json")"
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
if [[ -n "${CLAST_JSON:-}" ]]; then
|
|
151
|
+
local obj
|
|
152
|
+
obj="$(jq -cn \
|
|
153
|
+
--arg session_id "$session_id" \
|
|
154
|
+
--arg project "$slug" \
|
|
155
|
+
--arg segment "$seg" \
|
|
156
|
+
--arg branch "$branch" \
|
|
157
|
+
--arg start "$start_ts" \
|
|
158
|
+
--arg end "$end_ts" \
|
|
159
|
+
--arg duration "$duration_str" \
|
|
160
|
+
--argjson msg_count_approx "$msgs" \
|
|
161
|
+
--arg snapshot_path "$snapshot" \
|
|
162
|
+
--arg day_bucket "$day_bucket" \
|
|
163
|
+
--argjson curated "$curated" \
|
|
164
|
+
--arg first_prompt "$first_prompt" \
|
|
165
|
+
--arg last_prompt "$last_prompt" \
|
|
166
|
+
'{
|
|
167
|
+
session_id: $session_id,
|
|
168
|
+
project: $project,
|
|
169
|
+
segment: $segment,
|
|
170
|
+
branch: (if $branch == "" then null else $branch end),
|
|
171
|
+
start: $start,
|
|
172
|
+
end: $end,
|
|
173
|
+
duration: (if $duration == "" then null else $duration end),
|
|
174
|
+
msg_count_approx: $msg_count_approx,
|
|
175
|
+
snapshot_path: $snapshot_path,
|
|
176
|
+
day_bucket: $day_bucket,
|
|
177
|
+
curated: $curated,
|
|
178
|
+
first_prompt: (if $first_prompt == "" then null else $first_prompt end),
|
|
179
|
+
last_prompt: (if $last_prompt == "" then null else $last_prompt end)
|
|
180
|
+
}')"
|
|
181
|
+
if (( include_turns == 1 )); then
|
|
182
|
+
obj="$(jq -c --argjson f "$first_turns_json" --argjson l "$last_turns_json" \
|
|
183
|
+
'. + {first_turns:$f, last_turns:$l}' <<<"$obj")"
|
|
184
|
+
fi
|
|
185
|
+
printf '%s\n' "$obj"
|
|
186
|
+
return 0
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
if [[ -z "${CLAST_QUIET:-}" ]]; then
|
|
190
|
+
printf 'session_id: %s\n' "$session_id"
|
|
191
|
+
printf 'project: %s\n' "$slug"
|
|
192
|
+
printf 'segment: %s\n' "$seg"
|
|
193
|
+
printf 'branch: %s\n' "$branch"
|
|
194
|
+
printf 'start: %s\n' "$(_clast_show_human_ts "$start_ts")"
|
|
195
|
+
printf 'end: %s\n' "$(_clast_show_human_ts "$end_ts")"
|
|
196
|
+
if [[ -n "$duration_str" ]]; then
|
|
197
|
+
printf 'duration: %s\n' "$duration_str"
|
|
198
|
+
fi
|
|
199
|
+
printf 'msg_count: %s (approx)\n' "$msgs"
|
|
200
|
+
printf 'snapshot: %s\n' "$abs_path"
|
|
201
|
+
if [[ "$curated" == "true" ]]; then
|
|
202
|
+
printf 'curated: yes\n'
|
|
203
|
+
else
|
|
204
|
+
printf 'curated: no\n'
|
|
205
|
+
fi
|
|
206
|
+
if [[ -n "$first_prompt" ]]; then
|
|
207
|
+
printf 'first_prompt: %s\n' "$first_prompt"
|
|
208
|
+
fi
|
|
209
|
+
if [[ -n "$last_prompt" ]]; then
|
|
210
|
+
printf 'last_prompt: %s\n' "$last_prompt"
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
if (( include_turns == 1 )); then
|
|
214
|
+
printf '\n## First %s turns\n' "$turn_count"
|
|
215
|
+
jq -r '.[] | "[\(.role)] \(.text)"' <<<"$first_turns_json"
|
|
216
|
+
printf '\n## Last %s turns\n' "$turn_count"
|
|
217
|
+
jq -r '.[] | "[\(.role)] \(.text)"' <<<"$last_turns_json"
|
|
218
|
+
fi
|
|
219
|
+
fi
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# _clast_show_user_messages <abs_path>
|
|
223
|
+
# Stream user-message text content, one per line. Empty lines dropped.
|
|
224
|
+
_clast_show_user_messages() {
|
|
225
|
+
local path="$1"
|
|
226
|
+
jq -r '
|
|
227
|
+
select((.role // .message.role) == "user")
|
|
228
|
+
| (.message.content // .content // empty)
|
|
229
|
+
| if type == "array" then map(.text? // "") | join(" ") else . end
|
|
230
|
+
| select(. != null and . != "")
|
|
231
|
+
' "$path" 2>/dev/null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# _clast_show_collect_turns <abs_path>
|
|
235
|
+
# Emit a JSON array of {role, text} for user+assistant text messages.
|
|
236
|
+
_clast_show_collect_turns() {
|
|
237
|
+
local path="$1"
|
|
238
|
+
jq -sc '
|
|
239
|
+
map(
|
|
240
|
+
select((.role // .message.role) as $r | $r == "user" or $r == "assistant")
|
|
241
|
+
| {
|
|
242
|
+
role: (.role // .message.role),
|
|
243
|
+
text: ((.message.content // .content // "") | if type == "array" then map(.text? // "") | join(" ") else . end)
|
|
244
|
+
}
|
|
245
|
+
| select(.text != null and .text != "")
|
|
246
|
+
)
|
|
247
|
+
' "$path" 2>/dev/null || printf '[]'
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# _clast_show_truncate <text>
|
|
251
|
+
# Truncate to 120 chars + … if longer.
|
|
252
|
+
_clast_show_truncate() {
|
|
253
|
+
local t="$1"
|
|
254
|
+
if (( ${#t} > 120 )); then
|
|
255
|
+
printf '%s…' "${t:0:120}"
|
|
256
|
+
else
|
|
257
|
+
printf '%s' "$t"
|
|
258
|
+
fi
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# _clast_show_human_ts <iso>
|
|
262
|
+
# "2026-05-30T14:30:30Z" → "2026-05-30 14:30:30"
|
|
263
|
+
_clast_show_human_ts() {
|
|
264
|
+
local ts="$1"
|
|
265
|
+
if [[ -z "$ts" ]]; then printf ''; return 0; fi
|
|
266
|
+
printf '%s %s' "${ts:0:10}" "${ts:11:8}"
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# _clast_show_format_duration <seconds>
|
|
270
|
+
_clast_show_format_duration() {
|
|
271
|
+
local s="$1"
|
|
272
|
+
if (( s < 0 )); then s=0; fi
|
|
273
|
+
local h=$((s / 3600))
|
|
274
|
+
local m=$(((s % 3600) / 60))
|
|
275
|
+
local sec=$((s % 60))
|
|
276
|
+
if (( h > 0 )); then
|
|
277
|
+
printf '%dh %dm' "$h" "$m"
|
|
278
|
+
elif (( m > 0 )); then
|
|
279
|
+
printf '%dm %ds' "$m" "$sec"
|
|
280
|
+
else
|
|
281
|
+
printf '%ds' "$sec"
|
|
282
|
+
fi
|
|
283
|
+
}
|