@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.
Files changed (39) hide show
  1. package/README.md +41 -38
  2. package/bin/clast +31 -122
  3. package/bin/clast-plumbing +180 -0
  4. package/examples/cron/clast-snapshot.service +2 -2
  5. package/examples/cron/clast-snapshot.timer +1 -1
  6. package/examples/cron/crontab.sample +4 -4
  7. package/examples/workflows/morning-briefing.md +17 -17
  8. package/hooks/snapshot.sh +5 -5
  9. package/lib/clast/clast-classify-lib.bash +68 -0
  10. package/lib/clast/clast-dismissed-lib.bash +76 -5
  11. package/lib/clast/clast-lib.bash +93 -8
  12. package/lib/clast/clast-manifest-lib.bash +58 -5
  13. package/lib/clast/clast-porcelain-lib.bash +230 -0
  14. package/lib/clast/clast-porcelain-subcommands/brief.bash +236 -0
  15. package/lib/clast/clast-porcelain-subcommands/retro.bash +236 -0
  16. package/lib/clast/clast-porcelain-subcommands/undismiss.bash +27 -0
  17. package/lib/clast/clast-porcelain-subcommands/wake.bash +514 -0
  18. package/lib/clast/clast-registry-lib.bash +164 -41
  19. package/lib/clast/clast-retro-lib.bash +397 -0
  20. package/lib/clast/clast-subcommands/doctor.bash +29 -13
  21. package/lib/clast/clast-subcommands/entries.bash +40 -50
  22. package/lib/clast/clast-subcommands/projects.bash +16 -20
  23. package/lib/clast/clast-subcommands/registry.bash +26 -11
  24. package/lib/clast/clast-subcommands/retro.bash +217 -0
  25. package/lib/clast/clast-subcommands/sessions.bash +193 -48
  26. package/lib/clast/clast-subcommands/show.bash +69 -12
  27. package/lib/clast/clast-subcommands/snapshot.bash +29 -11
  28. package/lib/clast/clast-subcommands/stats.bash +7 -2
  29. package/lib/clast/prompts/brief-system.md +11 -5
  30. package/lib/clast/prompts/brief-user.md +4 -1
  31. package/lib/clast/prompts/retro-summary-system.md +14 -0
  32. package/lib/clast/prompts/retro-summary-user.md +7 -0
  33. package/lib/clast/prompts/{day-wakeup-draft-system.md → wake-draft-system.md} +1 -1
  34. package/package.json +3 -3
  35. package/{.claude-plugin/skills/wakeup → skills/brief}/SKILL.md +22 -20
  36. package/{.claude-plugin/skills/day-wakeup → skills/wake}/SKILL.md +54 -29
  37. package/bin/clast-brief +0 -305
  38. package/bin/clast-wake +0 -579
  39. /package/lib/clast/prompts/{day-wakeup-draft-user.md → wake-draft-user.md} +0 -0
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@procrastivity/clast",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Capture, curate, and surface Claude Code session history across all your projects.",
5
5
  "homepage": "https://github.com/procrastivity/clast#readme",
6
6
  "bin": {
7
7
  "clast": "bin/clast",
8
- "clast-wake": "bin/clast-wake",
9
- "clast-brief": "bin/clast-brief"
8
+ "clast-plumbing": "bin/clast-plumbing"
10
9
  },
11
10
  "files": [
12
11
  "bin/",
13
12
  "lib/",
14
13
  ".claude-plugin/",
14
+ "skills/",
15
15
  "hooks/",
16
16
  "examples/",
17
17
  "README.md",
@@ -1,49 +1,51 @@
1
1
  ---
2
- name: wakeup
2
+ name: brief
3
3
  description: |
4
- Synthesize a briefing for the current project (or a named one) so the user can resume work without re-explaining context. Use when the user says "/wakeup", "wakeup", "wake up", "catch me up", "where was I", "what was I working on", "load last session", "resume", or otherwise signals they want prior context for the project they're about to work on. Optionally accepts a project slug like "/wakeup xesapps". Reads recent curated entries and today's breadcrumbs from `~/.claude/journal/` and produces a 2–5k-token briefing. This is the per-project read flow; for cross-project daily curation use /day-wakeup; for mid-session pivots use session-brief.
4
+ Synthesize a briefing for the current project (or a named one) so the user can resume work without re-explaining context. Use when the user says "/brief", "brief me", "catch me up", "where was I", "what was I working on", "load last session", "resume", or otherwise signals they want prior context for the project they're about to work on. Optionally accepts a project slug like "/brief xesapps". Reads recent curated entries and today's breadcrumbs from `~/.claude/journal/` and produces a 2–5k-token briefing. This is the per-project read flow; for cross-project daily curation use /wake; for mid-session pivots use session-brief.
5
5
  ---
6
6
 
7
- # Wakeup
7
+ # Brief
8
8
 
9
9
  Synthesize a briefing for the current (or named) project so the user can resume without re-explaining context.
10
10
 
11
11
  ## Why this exists
12
12
 
13
- `/day-wakeup` curates yesterday's work into entries. `/wakeup` reads those entries back when starting work in a specific repo. The two are complementary: one writes, one reads.
13
+ `/wake` curates yesterday's work into entries. `/brief` reads those entries back when starting work in a specific repo. The two are complementary: one writes, one reads.
14
14
 
15
- ## Step 0: Resolve the clast binary
15
+ ## Step 0: Resolve the clast-plumbing binary
16
16
 
17
- Before running any `clast` command, determine which binary to use. Run this
18
- once at the start and reuse the result for all commands in this skill:
17
+ This skill calls the deterministic core (`clast-plumbing`), not the
18
+ LLM-aware porcelain (`clast`). Determine the binary to use once at the
19
+ start and reuse the result for all commands in this skill:
19
20
 
20
21
  ```bash
21
- if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" && -x "$CLAUDE_PLUGIN_ROOT/bin/clast" ]]; then
22
- CLAST_BIN="$CLAUDE_PLUGIN_ROOT/bin/clast"
23
- elif command -v clast >/dev/null 2>&1; then
24
- CLAST_BIN="clast"
22
+ if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" && -x "$CLAUDE_PLUGIN_ROOT/bin/clast-plumbing" ]]; then
23
+ CLAST_BIN="$CLAUDE_PLUGIN_ROOT/bin/clast-plumbing"
24
+ elif command -v clast-plumbing >/dev/null 2>&1; then
25
+ CLAST_BIN="clast-plumbing"
25
26
  else
26
27
  _pdir="$(find ~/.claude -maxdepth 5 -name plugin.json -path '*/clast/.claude-plugin/*' -print -quit 2>/dev/null)"
27
28
  if [[ -n "$_pdir" ]]; then
28
- CLAST_BIN="$(cd "$(dirname "$_pdir")/../.." && pwd)/bin/clast"
29
+ CLAST_BIN="$(cd "$(dirname "$_pdir")/../.." && pwd)/bin/clast-plumbing"
29
30
  fi
30
31
  fi
31
32
  ```
32
33
 
33
- If `CLAST_BIN` is still empty, tell the user: "clast CLI not found. Install it
34
- with `npm i -g @procrastivity/clast` or see the README for other options."
34
+ If `CLAST_BIN` is still empty, tell the user: "clast-plumbing CLI not found.
35
+ Install it with `npm i -g @procrastivity/clast` or see the README for other
36
+ options."
35
37
 
36
- Use `$CLAST_BIN` in place of bare `clast` for all commands in this skill.
38
+ Use `$CLAST_BIN` in place of bare `clast-plumbing` for all commands in this skill.
37
39
 
38
40
  ## Step 1: Resolve the project
39
41
 
40
- If the user passed a slug as an argument (`/wakeup xesapps`), use it directly. Otherwise resolve from current working directory:
42
+ If the user passed a slug as an argument (`/brief xesapps`), use it directly. Otherwise resolve from current working directory:
41
43
 
42
44
  ```bash
43
45
  $CLAST_BIN registry resolve "$(pwd)"
44
46
  ```
45
47
 
46
- If `pwd` doesn't resolve and no slug was given: print "Not in a registered project. Run `clast registry add .` first, or invoke as `/wakeup <slug>`." and stop.
48
+ If `pwd` doesn't resolve and no slug was given: print "Not in a registered project. Run `clast-plumbing registry add .` first, or invoke as `/brief <slug>`." and stop.
47
49
 
48
50
  ## Step 2: Gather data
49
51
 
@@ -71,7 +73,7 @@ $CLAST_BIN entries read <entry-path>
71
73
  Using the **synthesis prompt** (see below), produce a briefing of 2–5k tokens. Structure:
72
74
 
73
75
  ```
74
- ## Wakeup briefing — <project>
76
+ ## Brief — <project>
75
77
 
76
78
  **Active thread:** <one-line from most recent entry's "Open threads" section, or "None">
77
79
 
@@ -99,11 +101,11 @@ End with one of:
99
101
 
100
102
  ## Step 4: Don't write anything
101
103
 
102
- Wakeup is read-only. Never invoke write-form subcommands (`entries write`, `breadcrumb '<text>'`, `snapshot`) from this skill.
104
+ Brief is read-only. Never invoke write-form subcommands (`entries write`, `breadcrumb '<text>'`, `snapshot`) from this skill.
103
105
 
104
106
  ## Edge cases
105
107
 
106
- - **No entries for project**: print "No curated entries for `<slug>` yet. Run `/day-wakeup` to process recent sessions, or run `clast sessions --project <slug>` to see what's available." and stop.
108
+ - **No entries for project**: print "No curated entries for `<slug>` yet. Run `/wake` to process recent sessions, or run `clast-plumbing sessions --project <slug>` to see what's available." and stop.
107
109
  - **Slug resolves but no entries and no sessions**: print "Project `<slug>` registered but has no journal activity yet."
108
110
  - **Today's session count > 5**: summarize ("worked 12 sessions today, most recent 16:22 on branch `loop-guard-ngram`") rather than listing all.
109
111
 
@@ -1,40 +1,42 @@
1
1
  ---
2
- name: day-wakeup
3
- description: 'Generate curated journal entries from yesterday''s Claude Code sessions across all projects. Use when the user says "/day-wakeup", "day wakeup", "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 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 /wakeup; for mid-session pivots use session-brief.'
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.'
4
4
  ---
5
5
 
6
- # Day Wakeup
6
+ # Wake
7
7
 
8
8
  Process yesterday's Claude Code sessions across all projects. For each session, generate a draft journal entry and walk the user through accepting/editing/skipping it.
9
9
 
10
10
  ## Why this exists
11
11
 
12
- Curation at end-of-session has high friction (the user wants to stop, not summarize). Curation at start-of-next-day has lower friction (fresh eyes, easier to decide what's worth keeping). `/day-wakeup` is that start-of-next-day flow.
12
+ Curation at end-of-session has high friction (the user wants to stop, not summarize). Curation at start-of-next-day has lower friction (fresh eyes, easier to decide what's worth keeping). `/wake` is that start-of-next-day flow.
13
13
 
14
- The transcripts themselves are captured automatically by the SessionStart hook + cron — the user never has to remember to log anything. What `/day-wakeup` does is **curate the captured transcripts into durable entries the user controls**, and prompt for promotion of decisions, common-issues, and workflows along the way.
14
+ The transcripts themselves are captured automatically by the SessionStart hook + cron — the user never has to remember to log anything. What `/wake` does is **curate the captured transcripts into durable entries the user controls**, and prompt for promotion of decisions, common-issues, and workflows along the way.
15
15
 
16
- ## Step 0: Resolve the clast binary
16
+ ## Step 0: Resolve the clast-plumbing binary
17
17
 
18
- Before running any `clast` command, determine which binary to use. Run this
19
- once at the start and reuse the result for all commands in this skill:
18
+ This skill calls the deterministic core (`clast-plumbing`), not the
19
+ LLM-aware porcelain (`clast`). Determine the binary to use once at the
20
+ start and reuse the result for all commands in this skill:
20
21
 
21
22
  ```bash
22
- if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" && -x "$CLAUDE_PLUGIN_ROOT/bin/clast" ]]; then
23
- CLAST_BIN="$CLAUDE_PLUGIN_ROOT/bin/clast"
24
- elif command -v clast >/dev/null 2>&1; then
25
- CLAST_BIN="clast"
23
+ if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" && -x "$CLAUDE_PLUGIN_ROOT/bin/clast-plumbing" ]]; then
24
+ CLAST_BIN="$CLAUDE_PLUGIN_ROOT/bin/clast-plumbing"
25
+ elif command -v clast-plumbing >/dev/null 2>&1; then
26
+ CLAST_BIN="clast-plumbing"
26
27
  else
27
28
  _pdir="$(find ~/.claude -maxdepth 5 -name plugin.json -path '*/clast/.claude-plugin/*' -print -quit 2>/dev/null)"
28
29
  if [[ -n "$_pdir" ]]; then
29
- CLAST_BIN="$(cd "$(dirname "$_pdir")/../.." && pwd)/bin/clast"
30
+ CLAST_BIN="$(cd "$(dirname "$_pdir")/../.." && pwd)/bin/clast-plumbing"
30
31
  fi
31
32
  fi
32
33
  ```
33
34
 
34
- If `CLAST_BIN` is still empty, tell the user: "clast CLI not found. Install it
35
- with `npm i -g @procrastivity/clast` or see the README for other options."
35
+ If `CLAST_BIN` is still empty, tell the user: "clast-plumbing CLI not found.
36
+ Install it with `npm i -g @procrastivity/clast` or see the README for other
37
+ options."
36
38
 
37
- Use `$CLAST_BIN` in place of bare `clast` for all commands in this skill.
39
+ Use `$CLAST_BIN` in place of bare `clast-plumbing` for all commands in this skill.
38
40
 
39
41
  ## Step 1: Ensure fresh data
40
42
 
@@ -56,6 +58,27 @@ $CLAST_BIN --json sessions --since -30d
56
58
 
57
59
  Filter to sessions with `curated: false` or `stale: true` (stale sessions were curated but their transcript was updated since). If none remain, print "Nothing to curate — all sessions are curated or dismissed." and stop.
58
60
 
61
+ ### Step 2a: Auto-dismiss no-op sessions (deterministic, no LLM)
62
+
63
+ Each session row carries a deterministic `substantive` flag (computed by
64
+ `clast-plumbing` from the transcript, cached at snapshot time). A session is
65
+ `substantive: false` when **Claude never replied** — an empty session, one that
66
+ contains only slash commands like `/clear`, `/model`, `/config`, or one abandoned
67
+ before any response. These are worthless to curate. (A session driven by a *custom*
68
+ slash command still has assistant replies, so it stays `substantive: true`.)
69
+
70
+ Before generating any drafts, auto-dismiss every uncurated session with
71
+ `substantive == false` (skip this if the user set `CLAST_WAKE_AUTODISMISS_NOOP=0`):
72
+
73
+ ```bash
74
+ $CLAST_BIN sessions dismiss <session-id> --reason "auto: no substantive content (empty / slash-command-only)"
75
+ ```
76
+
77
+ Do **not** call the LLM for these. Dismissal is reversible via `clast undismiss <id>`.
78
+ Remove them from the working set, keep a count, and report it in the final summary
79
+ (e.g. "Auto-dismissed 5 no-op session(s)."). If nothing substantive remains after this,
80
+ print "Nothing to curate — all remaining sessions were empty or slash-command-only." and stop.
81
+
59
82
  ### Triage when multiple days have uncurated sessions
60
83
 
61
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:
@@ -108,18 +131,19 @@ For each session in the list:
108
131
  5. Present the **promotion question** (see below) via AskUserQuestion.
109
132
 
110
133
  6. Handle the response:
111
- - **Accept** (any combination of accept-flavored options): pipe the draft to `clast entries write` via stdin.
134
+ - **Accept** (any combination of accept-flavored options): pipe the draft to `clast-plumbing entries write` via stdin.
112
135
  - **Edit**: prompt the user for what to change, regenerate the draft incorporating their feedback, loop.
113
136
  - **Skip**: do not write.
114
- - **Stop here**: end the entire `/day-wakeup` flow, leaving remaining sessions uncurated (user can resume tomorrow).
137
+ - **Stop here**: end the entire `/wake` flow, leaving remaining sessions uncurated (user can resume tomorrow).
115
138
 
116
139
  ## Step 4: Final summary
117
140
 
118
141
  After all sessions are processed (or user stopped early), print a summary:
119
142
 
120
143
  ```
121
- Day wakeup complete.
144
+ Wake complete.
122
145
  Curated: 3 sessions across 2 projects.
146
+ Auto-dismissed (no-op): 5 sessions.
123
147
  Skipped: 1 session.
124
148
  Remaining uncurated: 0.
125
149
 
@@ -128,15 +152,15 @@ Promoted:
128
152
  Common-issues: 0
129
153
  Workflows: 1
130
154
 
131
- Run `/wakeup <project>` to start working on a specific project today.
155
+ Run `/brief <project>` to start working on a specific project today.
132
156
  ```
133
157
 
134
158
  ## Draft generation prompt
135
159
 
136
- The prompt templates live in `lib/clast/prompts/` so they are shared with the standalone `clast-wake` script:
160
+ The prompt templates are installed alongside the plugin under `$CLAUDE_PLUGIN_ROOT/lib/clast/prompts/`:
137
161
 
138
- - **System prompt:** `lib/clast/prompts/day-wakeup-draft-system.md`
139
- - **User prompt template:** `lib/clast/prompts/day-wakeup-draft-user.md` (uses `{{placeholder}}` syntax)
162
+ - **System prompt:** `$CLAUDE_PLUGIN_ROOT/lib/clast/prompts/wake-draft-system.md`
163
+ - **User prompt template:** `$CLAUDE_PLUGIN_ROOT/lib/clast/prompts/wake-draft-user.md` (uses `{{placeholder}}` syntax)
140
164
 
141
165
  When generating each draft, read those files and substitute the placeholders with session data. The user prompt template uses these placeholders: `{{project}}`, `{{branch}}`, `{{start}}`, `{{end}}`, `{{msg_count}}`, `{{first_turns}}`, `{{last_turns}}`, `{{breadcrumbs}}`.
142
166
 
@@ -154,7 +178,7 @@ After showing the draft, present:
154
178
  - `Accept + promote workflow` — also write a workflow file
155
179
  - `Edit` — user wants to revise; will prompt for changes
156
180
  - `Skip` — do not write this entry
157
- - `Stop here` — end /day-wakeup entirely, leave remaining sessions uncurated
181
+ - `Stop here` — end /wake entirely, leave remaining sessions uncurated
158
182
 
159
183
  If `Skip` and `Stop here` are both selected, treat as `Stop here`. If `Edit` is selected alongside any accept option, treat as `Edit` first (the user wants to revise before accepting).
160
184
 
@@ -175,7 +199,7 @@ If the user selects `Edit`:
175
199
  When the user accepts:
176
200
 
177
201
  1. Extract the suggested tags from the draft (the user may have edited them).
178
- 2. Pipe the entry body (without the suggested-tags trailer) to `clast entries write`:
202
+ 2. Pipe the entry body (without the suggested-tags trailer) to `clast-plumbing entries write`:
179
203
 
180
204
  ```bash
181
205
  $CLAST_BIN entries write \
@@ -190,14 +214,15 @@ $CLAST_BIN entries write \
190
214
 
191
215
  3. If the write succeeds, append a one-line confirmation to the running summary.
192
216
 
193
- For promoted items (decisions, common-issues, workflows): currently these are tracked inside the entry's body for v1. **TODO for v1.1: separate `clast decisions write` / `clast common-issues write` / `clast workflows write` subcommands and a directory structure to match.** Note this in the user-facing summary so the user knows they're folded into the entry for now.
217
+ For promoted items (decisions, common-issues, workflows): currently these are tracked inside the entry's body for v1. **TODO for v1.1: separate `clast-plumbing decisions write` / `clast-plumbing common-issues write` / `clast-plumbing workflows write` subcommands and a directory structure to match.** Note this in the user-facing summary so the user knows they're folded into the entry for now.
194
218
 
195
- <!-- step-12 addition: v1 promotion section convention --> When folding promoted items into the accepted entry body, append `## Decision`, `## Common issue`, or `## Workflow` (h2, singular, capitalized), each followed by the prompted-for title (h3) and body before invoking `clast entries write`.
219
+ <!-- step-12 addition: v1 promotion section convention --> When folding promoted items into the accepted entry body, append `## Decision`, `## Common issue`, or `## Workflow` (h2, singular, capitalized), each followed by the prompted-for title (h3) and body before invoking `clast-plumbing entries write`.
196
220
 
197
221
  ## Edge cases
198
222
 
199
223
  - **No uncurated sessions**: print "Nothing to curate — all sessions are curated or dismissed." and stop.
224
+ - **All uncurated sessions are no-ops**: after Step 2a auto-dismisses them, nothing substantive remains — print "Nothing to curate — all remaining sessions were empty or slash-command-only." and stop.
200
225
  - **Multi-day backlog**: present the triage step (Step 2) so the user can choose scope before processing.
201
- - **`clast snapshot` fails**: warn the user, then attempt to proceed with whatever's already in the manifest.
202
- - **`clast show` fails for a specific session**: skip that session, note it in the final summary, continue with the rest.
226
+ - **`clast-plumbing snapshot` fails**: warn the user, then attempt to proceed with whatever's already in the manifest.
227
+ - **`clast-plumbing show` fails for a specific session**: skip that session, note it in the final summary, continue with the rest.
203
228
  - **User says "do them all without prompting"**: not a v1 feature. Each session gets its own AskUserQuestion. The friction is intentional — it's where curation happens.
package/bin/clast-brief DELETED
@@ -1,305 +0,0 @@
1
- #!/usr/bin/env bash
2
- # clast-brief — LLM-powered project briefing without Claude Code.
3
- #
4
- # Replicates the /wakeup plugin skill using an OpenAI-compatible
5
- # chat completions endpoint. Reads curated entries, breadcrumbs,
6
- # and today's sessions for a project, then synthesizes a briefing.
7
- #
8
- # Usage: clast-brief [<project-slug>]
9
- #
10
- # If no slug is given, resolves from the current working directory.
11
- #
12
- # Required env vars:
13
- # CLAST_LLM_BASE_URL — e.g. https://api.openai.com/v1
14
- # CLAST_LLM_API_KEY — bearer token
15
- # CLAST_LLM_MODEL — e.g. gpt-4o, llama3
16
- set -euo pipefail
17
-
18
- CLAST_BRIEF_DIR="$(cd "$(dirname "$(realpath "$0")")/.." && pwd)"
19
-
20
- # ── Helpers ──────────────────────────────────────────────────────────
21
-
22
- die() { printf 'clast-brief: %s\n' "$1" >&2; exit "${2:-1}"; }
23
- warn() { printf 'clast-brief: warning: %s\n' "$1" >&2; }
24
- info() { printf '%s\n' "$1"; }
25
-
26
- # ── Preflight ────────────────────────────────────────────────────────
27
-
28
- preflight() {
29
- for tool in clast curl jq; do
30
- if ! command -v "$tool" >/dev/null 2>&1; then
31
- die "required tool not found: $tool"
32
- fi
33
- done
34
-
35
- local missing=0
36
- if [[ -z "${CLAST_LLM_BASE_URL:-}" ]]; then
37
- warn "CLAST_LLM_BASE_URL not set"; missing=1
38
- fi
39
- if [[ -z "${CLAST_LLM_API_KEY:-}" ]]; then
40
- warn "CLAST_LLM_API_KEY not set"; missing=1
41
- fi
42
- if [[ -z "${CLAST_LLM_MODEL:-}" ]]; then
43
- warn "CLAST_LLM_MODEL not set"; missing=1
44
- fi
45
-
46
- if (( missing )); then
47
- cat >&2 <<'EOF'
48
-
49
- Set these env vars before running clast-brief:
50
-
51
- export CLAST_LLM_BASE_URL="https://api.openai.com/v1"
52
- export CLAST_LLM_API_KEY="sk-..."
53
- export CLAST_LLM_MODEL="gpt-4o"
54
-
55
- Or for a local model (ollama, vllm, etc.):
56
-
57
- export CLAST_LLM_BASE_URL="http://localhost:11434/v1"
58
- export CLAST_LLM_API_KEY="unused"
59
- export CLAST_LLM_MODEL="llama3"
60
- EOF
61
- exit 1
62
- fi
63
- }
64
-
65
- # ── LLM call ─────────────────────────────────────────────────────────
66
-
67
- llm_chat() {
68
- local system_msg="$1"
69
- local user_msg="$2"
70
-
71
- local payload
72
- payload="$(jq -cn \
73
- --arg model "$CLAST_LLM_MODEL" \
74
- --arg system "$system_msg" \
75
- --arg user "$user_msg" \
76
- '{
77
- model: $model,
78
- messages: [
79
- {role: "system", content: $system},
80
- {role: "user", content: $user}
81
- ],
82
- temperature: 0.3
83
- }')"
84
-
85
- local response http_code body
86
- response="$(curl -s -w '\n%{http_code}' \
87
- "${CLAST_LLM_BASE_URL}/chat/completions" \
88
- -H "Authorization: Bearer $CLAST_LLM_API_KEY" \
89
- -H "Content-Type: application/json" \
90
- -d "$payload" 2>&1)" || true
91
-
92
- http_code="$(tail -n1 <<<"$response")"
93
- body="$(sed '$d' <<<"$response")"
94
-
95
- if [[ "$http_code" != "200" ]]; then
96
- warn "LLM API returned HTTP $http_code"
97
- if [[ -n "$body" ]]; then
98
- local err_msg
99
- err_msg="$(jq -r '.error.message // .error // .' <<<"$body" 2>/dev/null || echo "$body")"
100
- warn "$err_msg"
101
- fi
102
- return 1
103
- fi
104
-
105
- local content
106
- content="$(jq -r '.choices[0].message.content // empty' <<<"$body" 2>/dev/null)" || {
107
- warn "failed to parse LLM response"
108
- return 1
109
- }
110
-
111
- if [[ -z "$content" ]]; then
112
- warn "LLM returned empty content"
113
- return 1
114
- fi
115
-
116
- printf '%s' "$content"
117
- }
118
-
119
- # ── Prompt template ──────────────────────────────────────────────────
120
-
121
- resolve_prompt_dir() {
122
- local dir="$CLAST_BRIEF_DIR/lib/clast/prompts"
123
- if [[ -d "$dir" ]]; then
124
- printf '%s' "$dir"
125
- return
126
- fi
127
- local installed
128
- for installed in /usr/local/lib/clast/prompts "$HOME/.local/lib/clast/prompts"; do
129
- if [[ -d "$installed" ]]; then
130
- printf '%s' "$installed"
131
- return
132
- fi
133
- done
134
- die "cannot find prompts directory (checked $dir and install paths)"
135
- }
136
-
137
- load_system_prompt() {
138
- local prompt_dir
139
- prompt_dir="$(resolve_prompt_dir)"
140
- local file="$prompt_dir/brief-system.md"
141
- if [[ ! -r "$file" ]]; then
142
- die "system prompt not found: $file"
143
- fi
144
- cat "$file"
145
- }
146
-
147
- build_user_prompt() {
148
- local project="$1" entries="$2" breadcrumbs="$3" sessions="$4"
149
-
150
- local prompt_dir
151
- prompt_dir="$(resolve_prompt_dir)"
152
- local template_file="$prompt_dir/brief-user.md"
153
-
154
- if [[ -r "$template_file" ]]; then
155
- local template
156
- template="$(cat "$template_file")"
157
- template="${template//\{\{project\}\}/${project}}"
158
- template="${template//\{\{entries\}\}/${entries:-None.}}"
159
- template="${template//\{\{breadcrumbs\}\}/${breadcrumbs:-None.}}"
160
- template="${template//\{\{sessions\}\}/${sessions:-None.}}"
161
- printf '%s' "$template"
162
- else
163
- warn "user prompt template not found: $template_file — using inline fallback"
164
- cat <<EOF
165
- Project: ${project}
166
-
167
- Recent curated entries (newest first):
168
- ${entries:-None.}
169
-
170
- Today's breadcrumbs for this project:
171
- ${breadcrumbs:-None.}
172
-
173
- Today's session activity for this project:
174
- ${sessions:-None.}
175
- EOF
176
- fi
177
- }
178
-
179
- # ── Resolve project ──────────────────────────────────────────────────
180
-
181
- resolve_project() {
182
- local slug="${1:-}"
183
-
184
- if [[ -n "$slug" ]]; then
185
- printf '%s' "$slug"
186
- return
187
- fi
188
-
189
- local resolved
190
- resolved="$(clast registry resolve "$(pwd)" 2>/dev/null)" || true
191
- if [[ -n "$resolved" ]]; then
192
- printf '%s' "$resolved"
193
- return
194
- fi
195
-
196
- die "Not in a registered project. Run \`clast registry add .\` first, or invoke as \`clast-brief <slug>\`."
197
- }
198
-
199
- # ── Gather data ──────────────────────────────────────────────────────
200
-
201
- gather_entries() {
202
- local project="$1"
203
- local entries_json
204
- entries_json="$(clast --json entries --project "$project" --limit 5 2>/dev/null)" || true
205
-
206
- if [[ -z "$entries_json" ]] || [[ "$(jq 'length' <<<"$entries_json" 2>/dev/null)" == "0" ]]; then
207
- return
208
- fi
209
-
210
- local n i entry_meta entry_path entry_body result=""
211
- n="$(jq 'length' <<<"$entries_json")"
212
-
213
- for (( i = 0; i < n; i++ )); do
214
- entry_meta="$(jq -c ".[$i]" <<<"$entries_json")"
215
- entry_path="$(jq -r '.path' <<<"$entry_meta")"
216
-
217
- entry_body="$(clast entries read "$entry_path" 2>/dev/null)" || true
218
- if [[ -z "$entry_body" ]]; then
219
- local title date
220
- title="$(jq -r '.title // "untitled"' <<<"$entry_meta")"
221
- date="$(jq -r '.date' <<<"$entry_meta")"
222
- entry_body="# $date — $title (body not available)"
223
- fi
224
-
225
- if [[ -n "$result" ]]; then
226
- result="${result}
227
-
228
- ---
229
-
230
- "
231
- fi
232
- result="${result}${entry_body}"
233
- done
234
-
235
- printf '%s' "$result"
236
- }
237
-
238
- gather_breadcrumbs() {
239
- local project="$1"
240
- clast breadcrumb --read --project "$project" --day today 2>/dev/null || true
241
- }
242
-
243
- gather_sessions() {
244
- local project="$1"
245
- local sessions_json
246
- sessions_json="$(clast --json sessions --day today --project "$project" 2>/dev/null)" || true
247
-
248
- if [[ -z "$sessions_json" ]] || [[ "$(jq 'length' <<<"$sessions_json" 2>/dev/null)" == "0" ]]; then
249
- return
250
- fi
251
-
252
- local n
253
- n="$(jq 'length' <<<"$sessions_json")"
254
-
255
- if (( n > 5 )); then
256
- local latest_start
257
- latest_start="$(jq -r 'sort_by(.start) | last | .start // ""' <<<"$sessions_json")"
258
- printf 'Worked %d sessions today, most recent at %s.' "$n" "${latest_start:11:5}"
259
- return
260
- fi
261
-
262
- jq -r '
263
- sort_by(.start) | .[] |
264
- "\(.start[11:16]) start: \(if .branch and .branch != "null" then .branch else "no branch" end), \(.msg_count_approx) messages"
265
- ' <<<"$sessions_json" 2>/dev/null || true
266
- }
267
-
268
- # ── Main ─────────────────────────────────────────────────────────────
269
-
270
- main() {
271
- preflight
272
-
273
- local project
274
- project="$(resolve_project "${1:-}")"
275
- info "Briefing for project: $project"
276
-
277
- info "Gathering context..."
278
-
279
- local entries breadcrumbs sessions
280
- entries="$(gather_entries "$project")"
281
- breadcrumbs="$(gather_breadcrumbs "$project")"
282
- sessions="$(gather_sessions "$project")"
283
-
284
- if [[ -z "$entries" && -z "$breadcrumbs" && -z "$sessions" ]]; then
285
- info "No curated entries, breadcrumbs, or sessions for \`$project\`."
286
- info "Run \`clast-wake\` to curate recent sessions first, or run \`clast sessions --project $project\` to see what's available."
287
- exit 0
288
- fi
289
-
290
- local system_prompt user_prompt
291
- system_prompt="$(load_system_prompt)"
292
- user_prompt="$(build_user_prompt "$project" "$entries" "$breadcrumbs" "$sessions")"
293
-
294
- info "Synthesizing briefing..."
295
- printf '\n'
296
-
297
- local briefing
298
- if ! briefing="$(llm_chat "$system_prompt" "$user_prompt")"; then
299
- die "LLM call failed"
300
- fi
301
-
302
- printf '%s\n' "$briefing"
303
- }
304
-
305
- main "$@"