@procrastivity/clast 0.0.3 → 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.
- package/.claude-plugin/skills/day-wakeup/SKILL.md +21 -19
- package/.claude-plugin/skills/wakeup/SKILL.md +15 -13
- package/README.md +32 -29
- package/bin/clast +22 -124
- package/bin/clast-plumbing +173 -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 +11 -11
- package/hooks/snapshot.sh +5 -5
- package/lib/clast/clast-dismissed-lib.bash +6 -5
- package/lib/clast/clast-lib.bash +5 -2
- package/lib/clast/clast-manifest-lib.bash +28 -5
- package/lib/clast/clast-porcelain-lib.bash +217 -0
- package/lib/clast/clast-porcelain-subcommands/brief.bash +167 -0
- package/{bin/clast-wake → lib/clast/clast-porcelain-subcommands/wake.bash} +87 -217
- package/lib/clast/clast-registry-lib.bash +32 -14
- package/lib/clast/clast-subcommands/projects.bash +7 -1
- package/lib/clast/clast-subcommands/sessions.bash +92 -47
- package/lib/clast/clast-subcommands/show.bash +15 -4
- package/lib/clast/clast-subcommands/snapshot.bash +12 -2
- package/lib/clast/clast-subcommands/stats.bash +7 -2
- package/package.json +2 -3
- package/bin/clast-brief +0 -305
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# clast cron sample for unattended transcript capture.
|
|
2
2
|
# Install with: crontab -l | { cat; cat examples/cron/crontab.sample; } | crontab -
|
|
3
|
-
# Edit the clast path if you installed somewhere other than /usr/local/bin.
|
|
3
|
+
# Edit the clast-plumbing path if you installed somewhere other than /usr/local/bin.
|
|
4
4
|
|
|
5
5
|
# Every hour at :05, capture any new transcripts.
|
|
6
6
|
# Idempotent and silent on no-op, so safe to run frequently.
|
|
7
|
-
5 * * * * /usr/local/bin/clast snapshot >/dev/null 2>&1
|
|
7
|
+
5 * * * * /usr/local/bin/clast-plumbing snapshot >/dev/null 2>&1
|
|
8
8
|
|
|
9
9
|
# Alternative: every 15 minutes for active users moving between many sessions.
|
|
10
|
-
# */15 * * * * /usr/local/bin/clast snapshot >/dev/null 2>&1
|
|
10
|
+
# */15 * * * * /usr/local/bin/clast-plumbing snapshot >/dev/null 2>&1
|
|
11
11
|
|
|
12
12
|
# Alternative: once daily for hands-off archival.
|
|
13
|
-
# 10 4 * * * /usr/local/bin/clast snapshot >/dev/null 2>&1
|
|
13
|
+
# 10 4 * * * /usr/local/bin/clast-plumbing snapshot >/dev/null 2>&1
|
|
14
14
|
|
|
15
15
|
# Alternative: no cron. Rely on the SessionStart hook installed with the plugin.
|
|
16
16
|
# The hook covers most use cases already; cron is only useful when you want capture without ever opening Claude.
|
|
@@ -6,8 +6,8 @@ and `infra-tools` checked a flaky shell lint issue.
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
# User: /day-wakeup
|
|
9
|
-
clast snapshot
|
|
10
|
-
clast --json sessions --day yesterday
|
|
9
|
+
clast-plumbing snapshot
|
|
10
|
+
clast-plumbing --json sessions --day yesterday
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
It filters to `curated: false`, groups by project, and shows:
|
|
@@ -23,8 +23,8 @@ xesapps 09:12 feature/field-normalize 58 messages
|
|
|
23
23
|
The skill reads session details and breadcrumbs:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
clast --json show 33333333-3333-4333-8333-333333333333 --full --turns 8
|
|
27
|
-
clast breadcrumb --read --project infra-tools --day yesterday
|
|
26
|
+
clast-plumbing --json show 33333333-3333-4333-8333-333333333333 --full --turns 8
|
|
27
|
+
clast-plumbing breadcrumb --read --project infra-tools --day yesterday
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
It drafts a short entry:
|
|
@@ -50,8 +50,8 @@ uncurated and can be revisited in a later `/day-wakeup`.
|
|
|
50
50
|
The skill reads the next transcript:
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
clast --json show 22222222-2222-4222-8222-222222222222 --full --turns 8
|
|
54
|
-
clast breadcrumb --read --project notes --day yesterday
|
|
53
|
+
clast-plumbing --json show 22222222-2222-4222-8222-222222222222 --full --turns 8
|
|
54
|
+
clast-plumbing breadcrumb --read --project notes --day yesterday
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
The user asks for a revision:
|
|
@@ -76,7 +76,7 @@ Tighten the note-entry template before using it for project briefings.
|
|
|
76
76
|
The skill writes through stdin:
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
|
-
clast entries write \
|
|
79
|
+
clast-plumbing entries write \
|
|
80
80
|
--session 22222222-2222-4222-8222-222222222222 \
|
|
81
81
|
--slug entry-template-cleanup \
|
|
82
82
|
--tags notes,template \
|
|
@@ -89,8 +89,8 @@ clast entries write \
|
|
|
89
89
|
The skill reads the session and breadcrumbs:
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
-
clast --json show 11111111-1111-4111-8111-111111111111 --full --turns 8
|
|
93
|
-
clast breadcrumb --read --project xesapps --day yesterday
|
|
92
|
+
clast-plumbing --json show 11111111-1111-4111-8111-111111111111 --full --turns 8
|
|
93
|
+
clast-plumbing breadcrumb --read --project xesapps --day yesterday
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
The user chooses `Accept + promote decision` for this draft:
|
|
@@ -112,7 +112,7 @@ items are folded into the entry body rather than written to separate decision,
|
|
|
112
112
|
common-issue, or workflow files.
|
|
113
113
|
|
|
114
114
|
```bash
|
|
115
|
-
clast entries write \
|
|
115
|
+
clast-plumbing entries write \
|
|
116
116
|
--session 11111111-1111-4111-8111-111111111111 \
|
|
117
117
|
--slug field-normalization-fix \
|
|
118
118
|
--tags xesapps,fields,regression \
|
|
@@ -133,5 +133,5 @@ Run `/wakeup <project>` to start working on a specific project today.
|
|
|
133
133
|
|
|
134
134
|
What this changes on disk:
|
|
135
135
|
`entries/2026-05-31-1340-notes-entry-template-cleanup.md` and `entries/2026-05-31-0912-xesapps-field-normalization-fix.md` were written.
|
|
136
|
-
`.manifest.jsonl` was already current from `clast snapshot`; curation does not append to it.
|
|
136
|
+
`.manifest.jsonl` was already current from `clast-plumbing snapshot`; curation does not append to it.
|
|
137
137
|
The skipped `infra-tools` session stays uncurated and remains eligible for a later `/day-wakeup`.
|
package/hooks/snapshot.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# hooks/snapshot.sh — SessionStart hook. Backgrounds `clast snapshot`.
|
|
2
|
+
# hooks/snapshot.sh — SessionStart hook. Backgrounds `clast-plumbing snapshot`.
|
|
3
3
|
# Idempotent. Best-effort: never propagates a non-zero exit.
|
|
4
4
|
# shellcheck shell=bash
|
|
5
5
|
|
|
@@ -11,10 +11,10 @@ _plugin_root="$(cd "$_snap_dir/.." && pwd)"
|
|
|
11
11
|
# Prefer the bundled binary (version-matched to this plugin).
|
|
12
12
|
# Fall back to a system-level install on PATH.
|
|
13
13
|
_clast=""
|
|
14
|
-
if [[ -x "$_plugin_root/bin/clast" ]]; then
|
|
15
|
-
_clast="$_plugin_root/bin/clast"
|
|
16
|
-
elif command -v clast >/dev/null 2>&1; then
|
|
17
|
-
_clast="clast"
|
|
14
|
+
if [[ -x "$_plugin_root/bin/clast-plumbing" ]]; then
|
|
15
|
+
_clast="$_plugin_root/bin/clast-plumbing"
|
|
16
|
+
elif command -v clast-plumbing >/dev/null 2>&1; then
|
|
17
|
+
_clast="clast-plumbing"
|
|
18
18
|
fi
|
|
19
19
|
|
|
20
20
|
if [[ -n "$_clast" ]]; then
|
|
@@ -51,12 +51,13 @@ clast_dismissed_set() {
|
|
|
51
51
|
return 0
|
|
52
52
|
fi
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
# Single jq pass over the whole file. Previously this forked one jq per
|
|
55
|
+
# line, which dominated when the dismissed log grew large. `fromjson?`
|
|
56
|
+
# silently drops malformed lines, matching the old per-line tolerance.
|
|
57
|
+
local sid
|
|
58
|
+
while IFS= read -r sid; do
|
|
58
59
|
[[ -n "$sid" ]] && _ref["$sid"]=1
|
|
59
|
-
done < "$dismissed_file"
|
|
60
|
+
done < <(jq -rR 'fromjson? | .session_id // empty' "$dismissed_file" 2>/dev/null)
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
# clast_dismissed_check <session-id> — exit 0 if dismissed, 1 if not.
|
package/lib/clast/clast-lib.bash
CHANGED
|
@@ -204,10 +204,10 @@ clast_version() {
|
|
|
204
204
|
# to stderr for usage errors.
|
|
205
205
|
clast_usage() {
|
|
206
206
|
cat <<'EOF'
|
|
207
|
-
clast — Claude Code session journal
|
|
207
|
+
clast-plumbing — Claude Code session journal (deterministic core)
|
|
208
208
|
|
|
209
209
|
Usage:
|
|
210
|
-
clast [GLOBAL FLAGS] <subcommand> [ARGS...]
|
|
210
|
+
clast-plumbing [GLOBAL FLAGS] <subcommand> [ARGS...]
|
|
211
211
|
|
|
212
212
|
Subcommands:
|
|
213
213
|
whereami Show current path, registry, and journal state
|
|
@@ -229,5 +229,8 @@ Global flags:
|
|
|
229
229
|
-q, --quiet Suppress informational stdout output
|
|
230
230
|
--journal-dir P Override ~/.claude/journal/ (env: CLAST_JOURNAL_DIR)
|
|
231
231
|
--projects-dir P Override ~/.claude/projects/ (env: CLAST_PROJECTS_DIR)
|
|
232
|
+
|
|
233
|
+
The user-facing porcelain (LLM-aware) is `clast`. Run `clast --help` for
|
|
234
|
+
the `wake` and `brief` subcommands.
|
|
232
235
|
EOF
|
|
233
236
|
}
|
|
@@ -26,13 +26,18 @@ _clast_manifest_now_iso() {
|
|
|
26
26
|
date -u -d "@$epoch" +%Y-%m-%dT%H:%M:%SZ
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
# clast_manifest_append <session-id> <source> <snapshot> <source-mtime> <source-size> <day-bucket>
|
|
29
|
+
# clast_manifest_append <session-id> <source> <snapshot> <source-mtime> <source-size> <day-bucket> <msg-count> <first-ts> <last-ts>
|
|
30
30
|
clast_manifest_append() {
|
|
31
|
-
if [[ $# -ne
|
|
32
|
-
clast_log_error "clast_manifest_append: expected
|
|
31
|
+
if [[ $# -ne 9 ]]; then
|
|
32
|
+
clast_log_error "clast_manifest_append: expected 9 args, got $#"
|
|
33
33
|
return 2
|
|
34
34
|
fi
|
|
35
35
|
local session_id="$1" source="$2" snapshot="$3" source_mtime="$4" source_size="$5" day_bucket="$6"
|
|
36
|
+
# Cached per-session metadata (step 21): msg_count is an integer line
|
|
37
|
+
# count; first_ts/last_ts are the transcript's first/last line timestamps
|
|
38
|
+
# ("" → stored as JSON null). Readers prefer these over re-reading the
|
|
39
|
+
# snapshot file, falling back to the file when a line predates the cache.
|
|
40
|
+
local msg_count="$7" first_ts="$8" last_ts="$9"
|
|
36
41
|
local field
|
|
37
42
|
for field in session_id source snapshot source_mtime source_size day_bucket; do
|
|
38
43
|
if [[ -z "${!field}" ]]; then
|
|
@@ -44,6 +49,10 @@ clast_manifest_append() {
|
|
|
44
49
|
clast_log_error "clast_manifest_append: source_size must be a non-negative integer, got '$source_size'"
|
|
45
50
|
return 2
|
|
46
51
|
fi
|
|
52
|
+
if ! [[ "$msg_count" =~ ^[0-9]+$ ]]; then
|
|
53
|
+
clast_log_error "clast_manifest_append: msg_count must be a non-negative integer, got '$msg_count'"
|
|
54
|
+
return 2
|
|
55
|
+
fi
|
|
47
56
|
|
|
48
57
|
local journal_dir
|
|
49
58
|
journal_dir="$(clast_journal_dir)"
|
|
@@ -62,7 +71,10 @@ clast_manifest_append() {
|
|
|
62
71
|
--arg source_mtime "$source_mtime" \
|
|
63
72
|
--argjson source_size "$source_size" \
|
|
64
73
|
--arg day_bucket "$day_bucket" \
|
|
65
|
-
|
|
74
|
+
--argjson msg_count "$msg_count" \
|
|
75
|
+
--arg first_ts "$first_ts" \
|
|
76
|
+
--arg last_ts "$last_ts" \
|
|
77
|
+
'{session_id: $session_id, source: $source, snapshot: $snapshot, captured_at: $captured_at, source_mtime: $source_mtime, source_size: $source_size, day_bucket: $day_bucket, msg_count: $msg_count, first_ts: (if $first_ts == "" then null else $first_ts end), last_ts: (if $last_ts == "" then null else $last_ts end)}')" || {
|
|
66
78
|
clast_log_error "clast_manifest_append: jq failed to build manifest line"
|
|
67
79
|
return 1
|
|
68
80
|
}
|
|
@@ -168,6 +180,7 @@ clast_manifest_rebuild_from_disk() {
|
|
|
168
180
|
local count=0
|
|
169
181
|
if [[ -d "$transcripts_root" ]]; then
|
|
170
182
|
local snapshot day session_id mtime_epoch mtime_iso line
|
|
183
|
+
local msg_count first_ts last_ts
|
|
171
184
|
# source / source_size are unrecoverable: the snapshot file's size is
|
|
172
185
|
# the captured copy's size, not the original source's size at capture
|
|
173
186
|
# time, and the source path is not encoded in the snapshot itself.
|
|
@@ -185,13 +198,23 @@ clast_manifest_rebuild_from_disk() {
|
|
|
185
198
|
rm -f "$tmp_file"
|
|
186
199
|
return 1
|
|
187
200
|
fi
|
|
201
|
+
# source / source_size are unrecoverable, but the cached metadata
|
|
202
|
+
# (step 21) IS recoverable from the snapshot copy — compute it so
|
|
203
|
+
# doctor --fix backfills rather than emitting legacy-shaped lines.
|
|
204
|
+
first_ts="$(head -n1 "$snapshot" 2>/dev/null | jq -r '.timestamp // empty' 2>/dev/null || true)"
|
|
205
|
+
last_ts="$(tail -n1 "$snapshot" 2>/dev/null | jq -r '.timestamp // empty' 2>/dev/null || true)"
|
|
206
|
+
msg_count="$(wc -l <"$snapshot" 2>/dev/null | tr -d ' ')"
|
|
207
|
+
[[ "$msg_count" =~ ^[0-9]+$ ]] || msg_count=0
|
|
188
208
|
line="$(jq -c -n \
|
|
189
209
|
--arg session_id "$session_id" \
|
|
190
210
|
--arg snapshot "${snapshot#"$journal_dir/"}" \
|
|
191
211
|
--arg captured_at "$mtime_iso" \
|
|
192
212
|
--arg source_mtime "$mtime_iso" \
|
|
193
213
|
--arg day_bucket "$day" \
|
|
194
|
-
|
|
214
|
+
--argjson msg_count "$msg_count" \
|
|
215
|
+
--arg first_ts "$first_ts" \
|
|
216
|
+
--arg last_ts "$last_ts" \
|
|
217
|
+
'{session_id: $session_id, source: null, snapshot: $snapshot, captured_at: $captured_at, source_mtime: $source_mtime, source_size: 0, day_bucket: $day_bucket, msg_count: $msg_count, first_ts: (if $first_ts == "" then null else $first_ts end), last_ts: (if $last_ts == "" then null else $last_ts end)}')" || {
|
|
195
218
|
clast_log_error "clast_manifest_rebuild_from_disk: jq failed for '$snapshot'"
|
|
196
219
|
rm -f "$tmp_file"
|
|
197
220
|
return 1
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# clast-porcelain-lib.bash — shared helpers for the `clast` porcelain.
|
|
2
|
+
#
|
|
3
|
+
# The porcelain is the LLM-aware, user-facing layer that sits on top of
|
|
4
|
+
# `clast-plumbing`. This file holds helpers reused by every porcelain
|
|
5
|
+
# subcommand: error/info plumbing, an OpenAI-compatible chat call, and the
|
|
6
|
+
# prompt-template loader. Pure bash + jq + curl.
|
|
7
|
+
#
|
|
8
|
+
# Subcommand files live in $CLAST_LIB/clast-porcelain-subcommands/<name>.bash
|
|
9
|
+
# and each defines a single function `clast_cmd_<name>`.
|
|
10
|
+
# shellcheck shell=bash
|
|
11
|
+
|
|
12
|
+
# --- Helpers -----------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
clast_porcelain_die() {
|
|
15
|
+
printf 'clast: %s\n' "$1" >&2
|
|
16
|
+
exit "${2:-1}"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
clast_porcelain_warn() { printf 'clast: warning: %s\n' "$1" >&2; }
|
|
20
|
+
clast_porcelain_info() { printf '%s\n' "$1"; }
|
|
21
|
+
|
|
22
|
+
clast_porcelain_log_error() { printf 'clast: error: %s\n' "$1" >&2; }
|
|
23
|
+
|
|
24
|
+
# --- Version / usage ---------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
# Reuses the plumbing's clast_version helper if available (loaded by sourcing
|
|
27
|
+
# clast-lib.bash). When the porcelain is invoked stand-alone we still want a
|
|
28
|
+
# version string, so fall back to reading package.json directly.
|
|
29
|
+
clast_porcelain_version() {
|
|
30
|
+
if declare -F clast_version >/dev/null 2>&1; then
|
|
31
|
+
clast_version
|
|
32
|
+
return
|
|
33
|
+
fi
|
|
34
|
+
local pkg_root pkg
|
|
35
|
+
pkg_root="${CLAST_LIB:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
36
|
+
if [[ -f "$pkg_root/package.json" ]]; then
|
|
37
|
+
pkg="$pkg_root/package.json"
|
|
38
|
+
elif [[ -f "$pkg_root/../../package.json" ]]; then
|
|
39
|
+
pkg="$pkg_root/../../package.json"
|
|
40
|
+
else
|
|
41
|
+
clast_porcelain_log_error "clast_porcelain_version: package.json not found"
|
|
42
|
+
return 1
|
|
43
|
+
fi
|
|
44
|
+
jq -r '.version' "$pkg"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clast_porcelain_usage() {
|
|
48
|
+
cat <<'EOF'
|
|
49
|
+
clast — Claude Code session porcelain (LLM-aware)
|
|
50
|
+
|
|
51
|
+
Usage:
|
|
52
|
+
clast [GLOBAL FLAGS] <subcommand> [ARGS...]
|
|
53
|
+
|
|
54
|
+
Subcommands:
|
|
55
|
+
wake Interactive day curation (standalone equivalent of /day-wakeup)
|
|
56
|
+
brief Project briefing (standalone equivalent of /wakeup)
|
|
57
|
+
|
|
58
|
+
Global flags:
|
|
59
|
+
-h, --help Print this usage and exit
|
|
60
|
+
--version Print version and exit
|
|
61
|
+
|
|
62
|
+
Env (required by `wake` and `brief`):
|
|
63
|
+
CLAST_LLM_BASE_URL e.g. https://api.openai.com/v1
|
|
64
|
+
CLAST_LLM_API_KEY bearer token
|
|
65
|
+
CLAST_LLM_MODEL e.g. gpt-4o, llama3
|
|
66
|
+
|
|
67
|
+
The deterministic core (whereami / snapshot / sessions / entries / …)
|
|
68
|
+
lives in `clast-plumbing`. Run `clast-plumbing --help` for that surface.
|
|
69
|
+
EOF
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# --- Preflight ---------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
# clast_porcelain_preflight_llm — verify curl, jq, and the LLM env vars exist.
|
|
75
|
+
# Subcommands that don't call the LLM (none right now) can skip this.
|
|
76
|
+
clast_porcelain_preflight_llm() {
|
|
77
|
+
for tool in clast-plumbing curl jq; do
|
|
78
|
+
if ! command -v "$tool" >/dev/null 2>&1; then
|
|
79
|
+
clast_porcelain_die "required tool not found: $tool"
|
|
80
|
+
fi
|
|
81
|
+
done
|
|
82
|
+
|
|
83
|
+
local missing=0
|
|
84
|
+
if [[ -z "${CLAST_LLM_BASE_URL:-}" ]]; then
|
|
85
|
+
clast_porcelain_warn "CLAST_LLM_BASE_URL not set"; missing=1
|
|
86
|
+
fi
|
|
87
|
+
if [[ -z "${CLAST_LLM_API_KEY:-}" ]]; then
|
|
88
|
+
clast_porcelain_warn "CLAST_LLM_API_KEY not set"; missing=1
|
|
89
|
+
fi
|
|
90
|
+
if [[ -z "${CLAST_LLM_MODEL:-}" ]]; then
|
|
91
|
+
clast_porcelain_warn "CLAST_LLM_MODEL not set"; missing=1
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if (( missing )); then
|
|
95
|
+
cat >&2 <<'EOF'
|
|
96
|
+
|
|
97
|
+
Set these env vars before running clast wake / clast brief:
|
|
98
|
+
|
|
99
|
+
export CLAST_LLM_BASE_URL="https://api.openai.com/v1"
|
|
100
|
+
export CLAST_LLM_API_KEY="sk-..."
|
|
101
|
+
export CLAST_LLM_MODEL="gpt-4o"
|
|
102
|
+
|
|
103
|
+
Or for a local model (ollama, vllm, etc.):
|
|
104
|
+
|
|
105
|
+
export CLAST_LLM_BASE_URL="http://localhost:11434/v1"
|
|
106
|
+
export CLAST_LLM_API_KEY="unused"
|
|
107
|
+
export CLAST_LLM_MODEL="llama3"
|
|
108
|
+
EOF
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# --- LLM call ----------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
# clast_porcelain_llm_chat <system-msg> <user-msg>
|
|
116
|
+
# POST a chat-completions request to $CLAST_LLM_BASE_URL/chat/completions
|
|
117
|
+
# and echo the assistant's text on stdout. Returns nonzero on HTTP error or
|
|
118
|
+
# empty response.
|
|
119
|
+
clast_porcelain_llm_chat() {
|
|
120
|
+
local system_msg="$1"
|
|
121
|
+
local user_msg="$2"
|
|
122
|
+
|
|
123
|
+
local payload
|
|
124
|
+
payload="$(jq -cn \
|
|
125
|
+
--arg model "$CLAST_LLM_MODEL" \
|
|
126
|
+
--arg system "$system_msg" \
|
|
127
|
+
--arg user "$user_msg" \
|
|
128
|
+
'{
|
|
129
|
+
model: $model,
|
|
130
|
+
messages: [
|
|
131
|
+
{role: "system", content: $system},
|
|
132
|
+
{role: "user", content: $user}
|
|
133
|
+
],
|
|
134
|
+
temperature: 0.3
|
|
135
|
+
}')"
|
|
136
|
+
|
|
137
|
+
local response http_code body
|
|
138
|
+
response="$(curl -s -w '\n%{http_code}' \
|
|
139
|
+
"${CLAST_LLM_BASE_URL}/chat/completions" \
|
|
140
|
+
-H "Authorization: Bearer $CLAST_LLM_API_KEY" \
|
|
141
|
+
-H "Content-Type: application/json" \
|
|
142
|
+
-d "$payload" 2>&1)" || true
|
|
143
|
+
|
|
144
|
+
http_code="$(tail -n1 <<<"$response")"
|
|
145
|
+
body="$(sed '$d' <<<"$response")"
|
|
146
|
+
|
|
147
|
+
if [[ "$http_code" != "200" ]]; then
|
|
148
|
+
clast_porcelain_warn "LLM API returned HTTP $http_code"
|
|
149
|
+
if [[ -n "$body" ]]; then
|
|
150
|
+
local err_msg
|
|
151
|
+
err_msg="$(jq -r '.error.message // .error // .' <<<"$body" 2>/dev/null || echo "$body")"
|
|
152
|
+
clast_porcelain_warn "$err_msg"
|
|
153
|
+
fi
|
|
154
|
+
return 1
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
local content
|
|
158
|
+
content="$(jq -r '.choices[0].message.content // empty' <<<"$body" 2>/dev/null)" || {
|
|
159
|
+
clast_porcelain_warn "failed to parse LLM response"
|
|
160
|
+
return 1
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if [[ -z "$content" ]]; then
|
|
164
|
+
clast_porcelain_warn "LLM returned empty content"
|
|
165
|
+
return 1
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
printf '%s' "$content"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# --- Prompt templates --------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
# clast_porcelain_resolve_prompt_dir — echo the prompts dir, search order:
|
|
174
|
+
# 1. $CLAST_LIB/prompts (in-repo or installed alongside the lib)
|
|
175
|
+
# 2. /usr/local/lib/clast/prompts
|
|
176
|
+
# 3. $HOME/.local/lib/clast/prompts
|
|
177
|
+
clast_porcelain_resolve_prompt_dir() {
|
|
178
|
+
if [[ -n "${CLAST_LIB:-}" && -d "$CLAST_LIB/prompts" ]]; then
|
|
179
|
+
printf '%s' "$CLAST_LIB/prompts"
|
|
180
|
+
return
|
|
181
|
+
fi
|
|
182
|
+
local installed
|
|
183
|
+
for installed in /usr/local/lib/clast/prompts "$HOME/.local/lib/clast/prompts"; do
|
|
184
|
+
if [[ -d "$installed" ]]; then
|
|
185
|
+
printf '%s' "$installed"
|
|
186
|
+
return
|
|
187
|
+
fi
|
|
188
|
+
done
|
|
189
|
+
clast_porcelain_die "cannot find prompts directory (checked \$CLAST_LIB/prompts and install paths)"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# clast_porcelain_load_system_prompt <basename>
|
|
193
|
+
# Read the named system prompt (without extension) from the resolved
|
|
194
|
+
# prompts dir, e.g. clast_porcelain_load_system_prompt brief-system.
|
|
195
|
+
clast_porcelain_load_system_prompt() {
|
|
196
|
+
local name="$1"
|
|
197
|
+
local prompt_dir file
|
|
198
|
+
prompt_dir="$(clast_porcelain_resolve_prompt_dir)"
|
|
199
|
+
file="$prompt_dir/${name}.md"
|
|
200
|
+
if [[ ! -r "$file" ]]; then
|
|
201
|
+
clast_porcelain_die "system prompt not found: $file"
|
|
202
|
+
fi
|
|
203
|
+
cat "$file"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# clast_porcelain_user_prompt_file <basename>
|
|
207
|
+
# Echo the absolute path to the named user-prompt template, or empty string
|
|
208
|
+
# if missing. Callers do their own placeholder substitution.
|
|
209
|
+
clast_porcelain_user_prompt_file() {
|
|
210
|
+
local name="$1"
|
|
211
|
+
local prompt_dir file
|
|
212
|
+
prompt_dir="$(clast_porcelain_resolve_prompt_dir)"
|
|
213
|
+
file="$prompt_dir/${name}.md"
|
|
214
|
+
if [[ -r "$file" ]]; then
|
|
215
|
+
printf '%s' "$file"
|
|
216
|
+
fi
|
|
217
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# clast brief — LLM-powered project briefing.
|
|
2
|
+
#
|
|
3
|
+
# Replicates the /wakeup plugin skill using an OpenAI-compatible chat
|
|
4
|
+
# completions endpoint. Reads curated entries, breadcrumbs, and today's
|
|
5
|
+
# sessions for a project (via clast-plumbing), then synthesizes a briefing.
|
|
6
|
+
#
|
|
7
|
+
# Usage: clast brief [<project-slug>]
|
|
8
|
+
# If no slug is given, resolves from the current working directory.
|
|
9
|
+
# shellcheck shell=bash
|
|
10
|
+
|
|
11
|
+
# --- Resolve project ---------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
_clast_brief_resolve_project() {
|
|
14
|
+
local slug="${1:-}"
|
|
15
|
+
|
|
16
|
+
if [[ -n "$slug" ]]; then
|
|
17
|
+
printf '%s' "$slug"
|
|
18
|
+
return
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
local resolved
|
|
22
|
+
resolved="$(clast-plumbing registry resolve "$(pwd)" 2>/dev/null)" || true
|
|
23
|
+
if [[ -n "$resolved" ]]; then
|
|
24
|
+
printf '%s' "$resolved"
|
|
25
|
+
return
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
clast_porcelain_die "Not in a registered project. Run \`clast-plumbing registry add .\` first, or invoke as \`clast brief <slug>\`."
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# --- Gather data -------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
_clast_brief_gather_entries() {
|
|
34
|
+
local project="$1"
|
|
35
|
+
local entries_json
|
|
36
|
+
entries_json="$(clast-plumbing --json entries --project "$project" --limit 5 2>/dev/null)" || true
|
|
37
|
+
|
|
38
|
+
if [[ -z "$entries_json" ]] || [[ "$(jq 'length' <<<"$entries_json" 2>/dev/null)" == "0" ]]; then
|
|
39
|
+
return
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
local n i entry_meta entry_path entry_body result=""
|
|
43
|
+
n="$(jq 'length' <<<"$entries_json")"
|
|
44
|
+
|
|
45
|
+
for (( i = 0; i < n; i++ )); do
|
|
46
|
+
entry_meta="$(jq -c ".[$i]" <<<"$entries_json")"
|
|
47
|
+
entry_path="$(jq -r '.path' <<<"$entry_meta")"
|
|
48
|
+
|
|
49
|
+
entry_body="$(clast-plumbing entries read "$entry_path" 2>/dev/null)" || true
|
|
50
|
+
if [[ -z "$entry_body" ]]; then
|
|
51
|
+
local title date
|
|
52
|
+
title="$(jq -r '.title // "untitled"' <<<"$entry_meta")"
|
|
53
|
+
date="$(jq -r '.date' <<<"$entry_meta")"
|
|
54
|
+
entry_body="# $date — $title (body not available)"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [[ -n "$result" ]]; then
|
|
58
|
+
result="${result}
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
"
|
|
63
|
+
fi
|
|
64
|
+
result="${result}${entry_body}"
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
printf '%s' "$result"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_clast_brief_gather_breadcrumbs() {
|
|
71
|
+
local project="$1"
|
|
72
|
+
clast-plumbing breadcrumb --read --project "$project" --day today 2>/dev/null || true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_clast_brief_gather_sessions() {
|
|
76
|
+
local project="$1"
|
|
77
|
+
local sessions_json
|
|
78
|
+
sessions_json="$(clast-plumbing --json sessions --day today --project "$project" 2>/dev/null)" || true
|
|
79
|
+
|
|
80
|
+
if [[ -z "$sessions_json" ]] || [[ "$(jq 'length' <<<"$sessions_json" 2>/dev/null)" == "0" ]]; then
|
|
81
|
+
return
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
local n
|
|
85
|
+
n="$(jq 'length' <<<"$sessions_json")"
|
|
86
|
+
|
|
87
|
+
if (( n > 5 )); then
|
|
88
|
+
local latest_start
|
|
89
|
+
latest_start="$(jq -r 'sort_by(.start) | last | .start // ""' <<<"$sessions_json")"
|
|
90
|
+
printf 'Worked %d sessions today, most recent at %s.' "$n" "${latest_start:11:5}"
|
|
91
|
+
return
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
jq -r '
|
|
95
|
+
sort_by(.start) | .[] |
|
|
96
|
+
"\(.start[11:16]) start: \(if .branch and .branch != "null" then .branch else "no branch" end), \(.msg_count_approx) messages"
|
|
97
|
+
' <<<"$sessions_json" 2>/dev/null || true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# --- User prompt -------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
_clast_brief_build_user_prompt() {
|
|
103
|
+
local project="$1" entries="$2" breadcrumbs="$3" sessions="$4"
|
|
104
|
+
|
|
105
|
+
local template_file template
|
|
106
|
+
template_file="$(clast_porcelain_user_prompt_file brief-user)"
|
|
107
|
+
|
|
108
|
+
if [[ -n "$template_file" ]]; then
|
|
109
|
+
template="$(cat "$template_file")"
|
|
110
|
+
template="${template//\{\{project\}\}/${project}}"
|
|
111
|
+
template="${template//\{\{entries\}\}/${entries:-None.}}"
|
|
112
|
+
template="${template//\{\{breadcrumbs\}\}/${breadcrumbs:-None.}}"
|
|
113
|
+
template="${template//\{\{sessions\}\}/${sessions:-None.}}"
|
|
114
|
+
printf '%s' "$template"
|
|
115
|
+
else
|
|
116
|
+
clast_porcelain_warn "user prompt template not found: brief-user.md — using inline fallback"
|
|
117
|
+
cat <<EOF
|
|
118
|
+
Project: ${project}
|
|
119
|
+
|
|
120
|
+
Recent curated entries (newest first):
|
|
121
|
+
${entries:-None.}
|
|
122
|
+
|
|
123
|
+
Today's breadcrumbs for this project:
|
|
124
|
+
${breadcrumbs:-None.}
|
|
125
|
+
|
|
126
|
+
Today's session activity for this project:
|
|
127
|
+
${sessions:-None.}
|
|
128
|
+
EOF
|
|
129
|
+
fi
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# --- Main --------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
clast_cmd_brief() {
|
|
135
|
+
clast_porcelain_preflight_llm
|
|
136
|
+
|
|
137
|
+
local project
|
|
138
|
+
project="$(_clast_brief_resolve_project "${1:-}")"
|
|
139
|
+
clast_porcelain_info "Briefing for project: $project"
|
|
140
|
+
|
|
141
|
+
clast_porcelain_info "Gathering context..."
|
|
142
|
+
|
|
143
|
+
local entries breadcrumbs sessions
|
|
144
|
+
entries="$(_clast_brief_gather_entries "$project")"
|
|
145
|
+
breadcrumbs="$(_clast_brief_gather_breadcrumbs "$project")"
|
|
146
|
+
sessions="$(_clast_brief_gather_sessions "$project")"
|
|
147
|
+
|
|
148
|
+
if [[ -z "$entries" && -z "$breadcrumbs" && -z "$sessions" ]]; then
|
|
149
|
+
clast_porcelain_info "No curated entries, breadcrumbs, or sessions for \`$project\`."
|
|
150
|
+
clast_porcelain_info "Run \`clast wake\` to curate recent sessions first, or run \`clast-plumbing sessions --project $project\` to see what's available."
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
local system_prompt user_prompt
|
|
155
|
+
system_prompt="$(clast_porcelain_load_system_prompt brief-system)"
|
|
156
|
+
user_prompt="$(_clast_brief_build_user_prompt "$project" "$entries" "$breadcrumbs" "$sessions")"
|
|
157
|
+
|
|
158
|
+
clast_porcelain_info "Synthesizing briefing..."
|
|
159
|
+
printf '\n'
|
|
160
|
+
|
|
161
|
+
local briefing
|
|
162
|
+
if ! briefing="$(clast_porcelain_llm_chat "$system_prompt" "$user_prompt")"; then
|
|
163
|
+
clast_porcelain_die "LLM call failed"
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
printf '%s\n' "$briefing"
|
|
167
|
+
}
|