@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/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@procrastivity/clast",
|
|
3
|
-
"version": "0.0.
|
|
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-
|
|
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:
|
|
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 "/
|
|
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
|
-
#
|
|
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
|
-
`/
|
|
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
|
-
|
|
18
|
-
|
|
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.
|
|
34
|
-
with `npm i -g @procrastivity/clast` or see the README for other
|
|
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 (`/
|
|
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 `/
|
|
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
|
-
##
|
|
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
|
-
|
|
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 `/
|
|
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:
|
|
3
|
-
description: 'Generate curated journal entries from yesterday''s Claude Code sessions across all projects. Use when the user says "/
|
|
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
|
-
#
|
|
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). `/
|
|
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 `/
|
|
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
|
-
|
|
19
|
-
|
|
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.
|
|
35
|
-
with `npm i -g @procrastivity/clast` or see the README for other
|
|
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 `/
|
|
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
|
-
|
|
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 `/
|
|
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
|
|
160
|
+
The prompt templates are installed alongside the plugin under `$CLAUDE_PLUGIN_ROOT/lib/clast/prompts/`:
|
|
137
161
|
|
|
138
|
-
- **System prompt:**
|
|
139
|
-
- **User prompt template:**
|
|
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 /
|
|
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 "$@"
|