@procrastivity/clast 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/clast ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env bash
2
+ # clast — main dispatcher.
3
+ #
4
+ # Parses global flags, sources libs, dispatches to a subcommand.
5
+ # Subcommand files live in $CLAST_LIB/clast-subcommands/<name>.bash and
6
+ # each defines a single function `clast_cmd_<name>`.
7
+ set -euo pipefail
8
+
9
+ CLAST_LIB="${CLAST_LIB:-$(dirname "$(realpath "$0")")/../lib/clast}"
10
+ export CLAST_LIB
11
+
12
+ # shellcheck source=lib/clast/clast-lib.bash
13
+ source "$CLAST_LIB/clast-lib.bash"
14
+ # shellcheck source=lib/clast/clast-decode-lib.bash
15
+ source "$CLAST_LIB/clast-decode-lib.bash"
16
+ # shellcheck source=lib/clast/clast-registry-lib.bash
17
+ source "$CLAST_LIB/clast-registry-lib.bash"
18
+ # shellcheck source=lib/clast/clast-manifest-lib.bash
19
+ source "$CLAST_LIB/clast-manifest-lib.bash"
20
+
21
+ # --- Global flag parsing -------------------------------------------------
22
+ #
23
+ # Walk argv before the subcommand name, peeling off any recognized global
24
+ # flag and setting the corresponding env var. Stop at the first non-flag
25
+ # arg (the subcommand) or at `--`. Remaining argv is passed to the
26
+ # subcommand untouched.
27
+ #
28
+ # `--json` and `--journal-dir`/`--projects-dir` are forwarded to the lib
29
+ # via env so subcommands can also accept them locally without re-parsing.
30
+
31
+ declare -a _CLAST_REMAINING=()
32
+
33
+ while [[ $# -gt 0 ]]; do
34
+ case "$1" in
35
+ -h|--help|help)
36
+ clast_usage
37
+ exit 0
38
+ ;;
39
+ --version)
40
+ echo "clast $(clast_version)"
41
+ exit 0
42
+ ;;
43
+ --json)
44
+ export CLAST_JSON=1
45
+ shift
46
+ ;;
47
+ -v|--verbose)
48
+ export CLAST_VERBOSE=1
49
+ shift
50
+ ;;
51
+ -q|--quiet)
52
+ export CLAST_QUIET=1
53
+ shift
54
+ ;;
55
+ --journal-dir)
56
+ if [[ $# -lt 2 ]]; then
57
+ clast_log_error "--journal-dir requires a value"
58
+ exit 2
59
+ fi
60
+ export CLAST_JOURNAL_DIR="$2"
61
+ shift 2
62
+ ;;
63
+ --journal-dir=*)
64
+ export CLAST_JOURNAL_DIR="${1#*=}"
65
+ shift
66
+ ;;
67
+ --projects-dir)
68
+ if [[ $# -lt 2 ]]; then
69
+ clast_log_error "--projects-dir requires a value"
70
+ exit 2
71
+ fi
72
+ export CLAST_PROJECTS_DIR="$2"
73
+ shift 2
74
+ ;;
75
+ --projects-dir=*)
76
+ export CLAST_PROJECTS_DIR="${1#*=}"
77
+ shift
78
+ ;;
79
+ --)
80
+ shift
81
+ _CLAST_REMAINING+=("$@")
82
+ break
83
+ ;;
84
+ -*)
85
+ clast_log_error "unknown global flag: $1"
86
+ clast_usage >&2
87
+ exit 2
88
+ ;;
89
+ *)
90
+ # First non-flag arg is the subcommand; stop parsing.
91
+ _CLAST_REMAINING+=("$@")
92
+ break
93
+ ;;
94
+ esac
95
+ done
96
+
97
+ if [[ ${#_CLAST_REMAINING[@]} -eq 0 ]]; then
98
+ clast_usage
99
+ exit 0
100
+ fi
101
+
102
+ set -- "${_CLAST_REMAINING[@]}"
103
+
104
+ cmd="$1"; shift
105
+
106
+ # --- Subcommand dispatch -------------------------------------------------
107
+
108
+ # Map of stubbed subcommands → step number that will implement them.
109
+ # Future steps replace each stub with a real `source` + dispatch below.
110
+ _clast_stub() {
111
+ local name="$1" step="$2"
112
+ clast_log_error "$name is not yet implemented (planned for step $step)"
113
+ exit 2
114
+ }
115
+
116
+ case "$cmd" in
117
+ whereami)
118
+ # shellcheck source=lib/clast/clast-subcommands/whereami.bash
119
+ source "$CLAST_LIB/clast-subcommands/whereami.bash"
120
+ clast_cmd_whereami "$@"
121
+ ;;
122
+ snapshot)
123
+ # shellcheck source=lib/clast/clast-subcommands/snapshot.bash
124
+ source "$CLAST_LIB/clast-subcommands/snapshot.bash"
125
+ clast_cmd_snapshot "$@"
126
+ ;;
127
+ projects)
128
+ # shellcheck source=lib/clast/clast-subcommands/projects.bash
129
+ source "$CLAST_LIB/clast-subcommands/projects.bash"
130
+ clast_cmd_projects "$@"
131
+ ;;
132
+ sessions)
133
+ # shellcheck source=lib/clast/clast-subcommands/sessions.bash
134
+ source "$CLAST_LIB/clast-subcommands/sessions.bash"
135
+ clast_cmd_sessions "$@"
136
+ ;;
137
+ show)
138
+ # shellcheck source=lib/clast/clast-subcommands/show.bash
139
+ source "$CLAST_LIB/clast-subcommands/show.bash"
140
+ clast_cmd_show "$@"
141
+ ;;
142
+ entries)
143
+ # shellcheck source=lib/clast/clast-subcommands/entries.bash
144
+ source "$CLAST_LIB/clast-subcommands/entries.bash"
145
+ clast_cmd_entries "$@"
146
+ ;;
147
+ breadcrumb)
148
+ # shellcheck source=lib/clast/clast-subcommands/breadcrumb.bash
149
+ source "$CLAST_LIB/clast-subcommands/breadcrumb.bash"
150
+ clast_cmd_breadcrumb "$@"
151
+ ;;
152
+ registry)
153
+ # shellcheck source=lib/clast/clast-subcommands/registry.bash
154
+ source "$CLAST_LIB/clast-subcommands/registry.bash"
155
+ clast_cmd_registry "$@"
156
+ ;;
157
+ stats)
158
+ # shellcheck source=lib/clast/clast-subcommands/stats.bash
159
+ source "$CLAST_LIB/clast-subcommands/stats.bash"
160
+ clast_cmd_stats "$@"
161
+ ;;
162
+ doctor)
163
+ # shellcheck source=lib/clast/clast-subcommands/doctor.bash
164
+ source "$CLAST_LIB/clast-subcommands/doctor.bash"
165
+ clast_cmd_doctor "$@"
166
+ ;;
167
+ *)
168
+ clast_log_error "unknown subcommand '$cmd'"
169
+ clast_usage >&2
170
+ exit 2
171
+ ;;
172
+ esac
@@ -0,0 +1,32 @@
1
+ # examples/config/config.toml.sample
2
+ #
3
+ # NOTE: TOML config loading is planned for v1.x; v1.0 reads only
4
+ # the equivalent environment variables. Set these in your shell
5
+ # profile to get the same effect today.
6
+ #
7
+ # Uncommenting values in this file is a no-op in v1.0 because the
8
+ # loader does not exist yet. Treat this file as documentation for
9
+ # the planned config surface.
10
+ #
11
+ # ~/.config/clast/config.toml
12
+
13
+ [paths]
14
+ # Override where clast reads JSONL transcripts from.
15
+ # Env equivalent: CLAST_PROJECTS_DIR
16
+ # projects_dir = "/Users/you/.claude/projects"
17
+
18
+ # Override where clast writes the journal.
19
+ # Env equivalent: CLAST_JOURNAL_DIR
20
+ # journal_dir = "/Users/you/.claude/journal"
21
+
22
+ [time]
23
+ # The "day" cutoff used by `clast stats`, `clast sessions`, and
24
+ # the /day-wakeup window. Sessions started before this time on
25
+ # date D count as belonging to date D-1.
26
+ # Env equivalent: CLAST_DAY_CUTOFF
27
+ # day_cutoff = "04:00"
28
+
29
+ [logging]
30
+ # Silence info logs (matches the global --quiet flag).
31
+ # Env equivalent: CLAST_QUIET=1
32
+ # quiet = false
@@ -0,0 +1,10 @@
1
+ # clast hourly snapshot service.
2
+ # Copy to ~/.config/systemd/user/clast-snapshot.service, edit ExecStart if needed,
3
+ # then enable the timer with: systemctl --user enable --now clast-snapshot.timer
4
+
5
+ [Unit]
6
+ Description=Capture Claude Code transcripts with clast
7
+
8
+ [Service]
9
+ Type=oneshot
10
+ ExecStart=/usr/local/bin/clast snapshot
@@ -0,0 +1,15 @@
1
+ # clast hourly snapshot timer.
2
+ # Copy to ~/.config/systemd/user/clast-snapshot.timer, then run:
3
+ # systemctl --user enable --now clast-snapshot.timer
4
+
5
+ [Unit]
6
+ Description=Run clast snapshot hourly
7
+
8
+ [Timer]
9
+ OnBootSec=5min
10
+ OnUnitActiveSec=1h
11
+ Persistent=true
12
+ Unit=clast-snapshot.service
13
+
14
+ [Install]
15
+ WantedBy=timers.target
@@ -0,0 +1,16 @@
1
+ # clast cron sample for unattended transcript capture.
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.
4
+
5
+ # Every hour at :05, capture any new transcripts.
6
+ # Idempotent and silent on no-op, so safe to run frequently.
7
+ 5 * * * * /usr/local/bin/clast snapshot >/dev/null 2>&1
8
+
9
+ # Alternative: every 15 minutes for active users moving between many sessions.
10
+ # */15 * * * * /usr/local/bin/clast snapshot >/dev/null 2>&1
11
+
12
+ # Alternative: once daily for hands-off archival.
13
+ # 10 4 * * * /usr/local/bin/clast snapshot >/dev/null 2>&1
14
+
15
+ # Alternative: no cron. Rely on the SessionStart hook installed with the plugin.
16
+ # The hook covers most use cases already; cron is only useful when you want capture without ever opening Claude.
@@ -0,0 +1,137 @@
1
+ # Morning Briefing Example
2
+
3
+ This is a realistic walkthrough of one `/day-wakeup` run. Yesterday, `xesapps`
4
+ finished a field-normalization fix, `notes` explored a journal template update,
5
+ and `infra-tools` checked a flaky shell lint issue.
6
+
7
+ ```bash
8
+ # User: /day-wakeup
9
+ clast snapshot
10
+ clast --json sessions --day yesterday
11
+ ```
12
+
13
+ It filters to `curated: false`, groups by project, and shows:
14
+
15
+ ```text
16
+ infra-tools 16:05 lint-shellcheck 17 messages
17
+ notes 13:40 main 31 messages
18
+ xesapps 09:12 feature/field-normalize 58 messages
19
+ ```
20
+
21
+ ## Session 1: infra-tools
22
+
23
+ The skill reads session details and breadcrumbs:
24
+
25
+ ```bash
26
+ clast --json show 33333333-3333-4333-8333-333333333333 --full --turns 8
27
+ clast breadcrumb --read --project infra-tools --day yesterday
28
+ ```
29
+
30
+ It drafts a short entry:
31
+
32
+ ```markdown
33
+ # Session: Shellcheck cleanup pass
34
+
35
+ ## Goal
36
+ Understand why the lint job failed on the helper scripts.
37
+
38
+ ## Open threads
39
+ - Decide whether to rewrite the helper or add a targeted shellcheck suppression.
40
+ ```
41
+
42
+ AskUserQuestion presents `Accept`, `Accept + promote decision`, `Accept + promote
43
+ common-issue`, `Accept + promote workflow`, `Edit`, `Skip`, and `Stop here`.
44
+
45
+ The user chooses `Skip`. The skill writes nothing; the session remains
46
+ uncurated and can be revisited in a later `/day-wakeup`.
47
+
48
+ ## Session 2: notes
49
+
50
+ The skill reads the next transcript:
51
+
52
+ ```bash
53
+ clast --json show 22222222-2222-4222-8222-222222222222 --full --turns 8
54
+ clast breadcrumb --read --project notes --day yesterday
55
+ ```
56
+
57
+ The user asks for a revision:
58
+
59
+ ```text
60
+ User chooses: Edit
61
+ Assistant asks: What should change?
62
+ User: Make the title about the entry template, not the markdown cleanup.
63
+ ```
64
+
65
+ The regenerated draft is accepted:
66
+
67
+ ```markdown
68
+ # Session: Entry template cleanup
69
+ ## Goal
70
+ Tighten the note-entry template before using it for project briefings.
71
+ ## What shipped
72
+ - Drafted a shorter `## Open threads` section.
73
+ - Removed duplicate wording from the template notes.
74
+ ```
75
+
76
+ The skill writes through stdin:
77
+
78
+ ```bash
79
+ clast entries write \
80
+ --session 22222222-2222-4222-8222-222222222222 \
81
+ --slug entry-template-cleanup \
82
+ --tags notes,template \
83
+ --title "Entry template cleanup" \
84
+ --body-stdin
85
+ ```
86
+
87
+ ## Session 3: xesapps
88
+
89
+ The skill reads the session and breadcrumbs:
90
+
91
+ ```bash
92
+ clast --json show 11111111-1111-4111-8111-111111111111 --full --turns 8
93
+ clast breadcrumb --read --project xesapps --day yesterday
94
+ ```
95
+
96
+ The user chooses `Accept + promote decision` for this draft:
97
+
98
+ ```markdown
99
+ # Session: Field normalization fix
100
+ ## Goal
101
+ Normalize field metadata before it reaches the consumer-field view.
102
+ ## What shipped
103
+ - Added the canonical field-name path.
104
+ - Merged the feature branch after the regression check passed.
105
+ ## Issues + fixes
106
+ - **Issue:** The first pass changed display labels instead of canonical names.
107
+ **Fix:** Moved normalization earlier and kept display labels unchanged.
108
+ ```
109
+
110
+ The skill asks for the promoted decision title and content. In v1.0, promoted
111
+ items are folded into the entry body rather than written to separate decision,
112
+ common-issue, or workflow files.
113
+
114
+ ```bash
115
+ clast entries write \
116
+ --session 11111111-1111-4111-8111-111111111111 \
117
+ --slug field-normalization-fix \
118
+ --tags xesapps,fields,regression \
119
+ --title "Field normalization fix" \
120
+ --body-stdin
121
+ ```
122
+
123
+ Final summary:
124
+
125
+ ```text
126
+ Day wakeup complete.
127
+ Curated: 2 sessions across 2 projects.
128
+ Skipped: 1 session.
129
+ Remaining uncurated: 1.
130
+ Promoted: 1 decision (folded into the accepted entry for v1.0).
131
+ Run `/wakeup <project>` to start working on a specific project today.
132
+ ```
133
+
134
+ What this changes on disk:
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.
137
+ The skipped `infra-tools` session stays uncurated and remains eligible for a later `/day-wakeup`.
@@ -0,0 +1,8 @@
1
+ {
2
+ "hooks": [
3
+ {
4
+ "event": "SessionStart",
5
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/snapshot.sh"
6
+ }
7
+ ]
8
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ # hooks/snapshot.sh
3
+ #
4
+ # Fired on Claude Code SessionStart. Backgrounds `clast snapshot` so it doesn't
5
+ # block session start. Silent if clast isn't installed — the plugin can still
6
+ # load cleanly even if the CLI isn't on PATH.
7
+ #
8
+ # Idempotent. Safe to run repeatedly. Best-effort: never propagates a non-zero
9
+ # exit to Claude Code (a failed snapshot is not a session-start failure).
10
+ # shellcheck shell=bash
11
+
12
+ if command -v clast >/dev/null 2>&1; then
13
+ (clast snapshot >/dev/null 2>&1 &)
14
+ fi
15
+ exit 0
File without changes
@@ -0,0 +1,184 @@
1
+ # clast-decode-lib.bash — segment ↔ path encoder/decoder
2
+ #
3
+ # Claude Code stores transcripts under ~/.claude/projects/<segment>/, where
4
+ # <segment> is the absolute project path with `/` replaced by `-`. The
5
+ # encoding is lossy: a literal `-` in the source path becomes a `-` in the
6
+ # segment, identical to a path separator. Decoding ambiguity is resolved
7
+ # against the filesystem (and, for git repos, a `git rev-parse` probe).
8
+ #
9
+ # See docs/overview.md#glossary for the "segment" term and
10
+ # docs/repo-bootstrap.md#libclastclast-decode-libbash for the algorithm.
11
+ # shellcheck shell=bash
12
+
13
+ if [[ -n "${_CLAST_DECODE_LIB_SOURCED:-}" ]]; then
14
+ return 0
15
+ fi
16
+ _CLAST_DECODE_LIB_SOURCED=1
17
+
18
+ # clast_encode_path <absolute-path>
19
+ # "/home/beau/code/xesapps" -> "-home-beau-code-xesapps"
20
+ # "C:/Users/Beast/foo" -> "C--Users-Beast-foo"
21
+ clast_encode_path() {
22
+ local path="$1"
23
+ if [[ -z "$path" ]]; then
24
+ printf '\n'
25
+ return 0
26
+ fi
27
+ # Windows-style drive letter: "C:/..." -> "C-/..." (one dash for the colon),
28
+ # then the standard `/` -> `-` replacement turns the leading slash into a
29
+ # second dash, giving the canonical "C--..." form.
30
+ if [[ "$path" =~ ^([A-Za-z]):(/.*)$ ]]; then
31
+ path="${BASH_REMATCH[1]}-${BASH_REMATCH[2]}"
32
+ fi
33
+ printf '%s\n' "${path//\//-}"
34
+ }
35
+
36
+ # _clast_split_segment <segment>
37
+ # Strips a leading dash and (optionally) a Windows drive prefix. Sets
38
+ # globals _CLAST_PREFIX and _CLAST_REST for the caller. Returns 0 on
39
+ # success, 1 on empty input.
40
+ #
41
+ # "-home-beau-foo" -> prefix="/", rest="home-beau-foo"
42
+ # "C--Users-Beast-foo" -> prefix="C:/", rest="Users-Beast-foo"
43
+ # "foo" -> prefix="/", rest="foo" (no leading dash)
44
+ _clast_split_segment() {
45
+ local seg="$1"
46
+ if [[ -z "$seg" ]]; then
47
+ return 1
48
+ fi
49
+ if [[ "$seg" =~ ^([A-Za-z])--(.*)$ ]]; then
50
+ _CLAST_PREFIX="${BASH_REMATCH[1]}:/"
51
+ _CLAST_REST="${BASH_REMATCH[2]}"
52
+ elif [[ "$seg" == -* ]]; then
53
+ _CLAST_PREFIX="/"
54
+ _CLAST_REST="${seg#-}"
55
+ else
56
+ _CLAST_PREFIX="/"
57
+ _CLAST_REST="$seg"
58
+ fi
59
+ return 0
60
+ }
61
+
62
+ # clast_decode_candidates <segment>
63
+ # Print every possible decoding, one per line. No filesystem checks.
64
+ # Order is deterministic: separator-heavy decodings first (more `/`),
65
+ # matching the naive decode at the top of the list.
66
+ clast_decode_candidates() {
67
+ local seg="$1"
68
+ if [[ -z "$seg" ]]; then
69
+ printf '\n'
70
+ return 0
71
+ fi
72
+ _clast_split_segment "$seg" || return 1
73
+ local prefix="$_CLAST_PREFIX" rest="$_CLAST_REST"
74
+
75
+ # Split rest on `-`. Each gap between tokens is either `/` (separator) or
76
+ # `-` (literal). Iterate bitmasks 0..(2^gaps - 1); bit set => literal dash.
77
+ local -a tokens=()
78
+ local IFS='-'
79
+ read -r -a tokens <<<"$rest"
80
+ unset IFS
81
+ local n=${#tokens[@]}
82
+ if (( n == 0 )); then
83
+ printf '%s\n' "$prefix"
84
+ return 0
85
+ fi
86
+ local gaps=$((n - 1))
87
+ local total=$((1 << gaps))
88
+ local mask i candidate
89
+ for (( mask = 0; mask < total; mask++ )); do
90
+ candidate="${tokens[0]}"
91
+ for (( i = 1; i < n; i++ )); do
92
+ if (( (mask >> (i - 1)) & 1 )); then
93
+ candidate+="-${tokens[i]}"
94
+ else
95
+ candidate+="/${tokens[i]}"
96
+ fi
97
+ done
98
+ printf '%s%s\n' "$prefix" "$candidate"
99
+ done
100
+ }
101
+
102
+ # _clast_naive_decode <segment>
103
+ # The "all dashes are separators" decoding — the first candidate.
104
+ _clast_naive_decode() {
105
+ local seg="$1"
106
+ if [[ -z "$seg" ]]; then
107
+ printf '\n'
108
+ return 0
109
+ fi
110
+ _clast_split_segment "$seg" || return 1
111
+ local rest="${_CLAST_REST//-/\/}"
112
+ printf '%s%s\n' "$_CLAST_PREFIX" "$rest"
113
+ }
114
+
115
+ # clast_decode_segment <segment>
116
+ # Print the resolved absolute path. Exit 0 if confident, 1 if the answer
117
+ # is the naive decode but disk evidence is missing (caller may surface).
118
+ clast_decode_segment() {
119
+ local seg="$1"
120
+ if [[ -z "$seg" ]]; then
121
+ printf '\n'
122
+ return 0
123
+ fi
124
+
125
+ local naive
126
+ naive="$(_clast_naive_decode "$seg")"
127
+
128
+ # 1. Naive decode resolves on disk → done.
129
+ if [[ -d "$naive" ]]; then
130
+ printf '%s\n' "$naive"
131
+ return 0
132
+ fi
133
+
134
+ # 2/3. Generate candidates, intersect with the filesystem.
135
+ local -a candidates=() existing=()
136
+ mapfile -t candidates < <(clast_decode_candidates "$seg")
137
+ local c
138
+ for c in "${candidates[@]}"; do
139
+ if [[ -d "$c" ]]; then
140
+ existing+=("$c")
141
+ fi
142
+ done
143
+
144
+ if (( ${#existing[@]} == 1 )); then
145
+ printf '%s\n' "${existing[0]}"
146
+ return 0
147
+ fi
148
+
149
+ if (( ${#existing[@]} > 1 )); then
150
+ # 3a. Consult sessions-index.json if present.
151
+ local idx projects_dir project_path
152
+ projects_dir="${CLAST_PROJECTS_DIR:-$HOME/.claude/projects}"
153
+ idx="$projects_dir/$seg/sessions-index.json"
154
+ if [[ -r "$idx" ]]; then
155
+ project_path="$(jq -r '.projectPath // empty' "$idx" 2>/dev/null || true)"
156
+ if [[ -n "$project_path" ]]; then
157
+ for c in "${existing[@]}"; do
158
+ if [[ "$c" == "$project_path" ]]; then
159
+ printf '%s\n' "$c"
160
+ return 0
161
+ fi
162
+ done
163
+ fi
164
+ fi
165
+ # 3b. git rev-parse probe.
166
+ local matches=()
167
+ for c in "${existing[@]}"; do
168
+ if git -C "$c" rev-parse --show-toplevel >/dev/null 2>&1; then
169
+ matches+=("$c")
170
+ fi
171
+ done
172
+ if (( ${#matches[@]} == 1 )); then
173
+ printf '%s\n' "${matches[0]}"
174
+ return 0
175
+ fi
176
+ # Still ambiguous — surface naive, caller decides.
177
+ printf '%s\n' "$naive"
178
+ return 1
179
+ fi
180
+
181
+ # 4. No candidate exists on disk.
182
+ printf '%s\n' "$naive"
183
+ return 1
184
+ }