@procrastivity/clast 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +11 -0
- package/.claude-plugin/skills/day-wakeup/SKILL.md +194 -0
- package/.claude-plugin/skills/wakeup/SKILL.md +115 -0
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/bin/.gitkeep +0 -0
- package/bin/clast +172 -0
- package/examples/config/config.toml.sample +32 -0
- package/examples/cron/clast-snapshot.service +10 -0
- package/examples/cron/clast-snapshot.timer +15 -0
- package/examples/cron/crontab.sample +16 -0
- package/examples/workflows/morning-briefing.md +137 -0
- package/hooks/hooks.json +8 -0
- package/hooks/snapshot.sh +15 -0
- package/lib/clast/.gitkeep +0 -0
- package/lib/clast/clast-decode-lib.bash +184 -0
- package/lib/clast/clast-lib.bash +233 -0
- package/lib/clast/clast-manifest-lib.bash +232 -0
- package/lib/clast/clast-registry-lib.bash +252 -0
- package/lib/clast/clast-subcommands/.gitkeep +0 -0
- package/lib/clast/clast-subcommands/breadcrumb.bash +454 -0
- package/lib/clast/clast-subcommands/doctor.bash +607 -0
- package/lib/clast/clast-subcommands/entries.bash +697 -0
- package/lib/clast/clast-subcommands/projects.bash +309 -0
- package/lib/clast/clast-subcommands/registry.bash +207 -0
- package/lib/clast/clast-subcommands/sessions.bash +275 -0
- package/lib/clast/clast-subcommands/show.bash +283 -0
- package/lib/clast/clast-subcommands/snapshot.bash +294 -0
- package/lib/clast/clast-subcommands/stats.bash +354 -0
- package/lib/clast/clast-subcommands/whereami.bash +108 -0
- package/package.json +39 -0
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`.
|
package/hooks/hooks.json
ADDED
|
@@ -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
|
+
}
|