Setting the file. One moment.
Find Sessions · Daily Recap · bitwarden/ai-plugins · Skills Docs
ContentsBack to the top of the page 46.10
README
scripts/find-sessions.sh
scripts/ find-sessions.sh
Shell · 137 lines · 5 KB
15 # session can run across several days.
16 #
17 # Environment:
18 # CLAUDE_CONFIG_DIR Claude Code config directory. Default: ~/.claude
19 #
20 # Output (stdout): JSON array of session digests, ordered by first activity.
21 #
22 # Requires: jq, and GNU or BSD date, find, and touch.
23
24 set -euo pipefail
25
26 SCRIPT_DIR = "$( cd "$( dirname "${ BASH_SOURCE [0]}")" && pwd )"
27 # shellcheck source=SCRIPTDIR/date-utils.sh
28 source "${ SCRIPT_DIR }/date-utils.sh"
29
30 die () {
31 echo "find-sessions: $* " >&2
32 exit 1
33 }
34
35 usage () {
36 echo "Usage: find-sessions.sh START_UTC END_UTC [--include-home]" >&2
37 exit 2
38 }
39
40 command -v jq > /dev/null 2>&1 || die "'jq' is required but not installed"
41
42 [[ $# -ge 2 && $# -le 3 ]] || usage
43 START_UTC = " $1 "
44 END_UTC = " $2 "
45 INCLUDE_HOME = false
46 if [[ $# -eq 3 ]]; then
47 [[ " $3 " == "--include-home" ]] || usage
48 INCLUDE_HOME = true
49 fi
50
51 iso_re = '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$'
52 [[ " $START_UTC " =~ $iso_re && " $END_UTC " =~ $iso_re ]] \
53 || die "timestamps must look like 2026-10-13T11:00:00Z"
54 [[ " $START_UTC " < " $END_UTC " ]] || die "START_UTC must be earlier than END_UTC"
55 START_EPOCH = "$( utc_to_epoch " $START_UTC ")" || die "could not parse ${ START_UTC }"
56
57 PROJECTS_DIR = "${ CLAUDE_CONFIG_DIR :- ${ HOME } / . claude }/projects"
58 [[ -d " $PROJECTS_DIR " ]] || die "no Claude Code session logs found at ${ PROJECTS_DIR }"
59
60 # Claude Code names each project directory after the launch path with every
61 # non-alphanumeric character replaced by '-'.
62 HOME_SLUG = "$( printf '%s' " $HOME " | sed 's/[^A-Za-z0-9]/-/g')"
63
64 WORK_DIR = "$( mktemp -d "${ TMPDIR :-/ tmp }/daily-recap.XXXXXX")"
65 trap 'rm -rf "$WORK_DIR"' EXIT
66
67 # A session with activity in the window was last written at or after the window start,
68 # so a modification-time pre-filter keeps the scan cheap without missing anything.
69 REF_FILE = "${ WORK_DIR }/window-start"
70 touch -t "$( epoch_fmt " $START_EPOCH " '%Y%m%d%H%M.%S')" " $REF_FILE "
71
72 DIGESTS = "${ WORK_DIR }/digests.jsonl"
73 : > " $DIGESTS "
74
75 while IFS = read -r -d '' session_file ; do
76 project = "$( basename "$( dirname " $session_file ")")"
77 case " $project " in
78 -tmp- * | -private-tmp- * | -var-folders- * | -private-var-folders- * ) continue ;;
79 esac
80 if [[ " $INCLUDE_HOME " != true && " $project " == " $HOME_SLUG " ]]; then
81 continue
82 fi
83 session = "$( basename " $session_file " .jsonl)"
84
85 # Read line by line and skip unparsable lines, since the newest line of a live
86 # session can be partially written. The session path is passed only as a label, never
87 # written, so SC2094 does not apply.
88 # shellcheck disable=SC2094
89 jq -n -R -c \
90 --arg s " $START_UTC " \
91 --arg e " $END_UTC " \
92 --arg project " $project " \
93 --arg session " $session " \
94 --arg path " $session_file " '
95 def text_of:
96 if type == "string" then .
97 elif type == "array" then [.[] | select(.type? == "text") | .text] | join("\n")
98 else "" end;
99
100 # Compare whole seconds: session timestamps carry milliseconds, and as raw strings
101 # "11:00:00.250Z" sorts before "11:00:00Z" because "." sorts before "Z".
102 def secs: .[0:19];
103
104 [ inputs | fromjson? | objects
105 | select((.timestamp | type) == "string")
106 | select((.timestamp | secs) >= ($s | secs) and (.timestamp | secs) < ($e | secs))
107 ] as $in
108 | if ($in | length) == 0 then empty else
109 ([ $in[]
110 | select(.type == "user" and (.isMeta | not) and (.isSidechain | not))
111 | .message.content | text_of
112 | select(test("\\S"))
113 | select(test("^\\s*(<(command-|local-command-|system-reminder|task-notification|agent-message)|Another Claude session sent a message:)") | not)
114 ]) as $prompts
115 | ([ $in[] | select(.type == "assistant") | .message.content[]? | objects
116 | select(.type == "tool_use") ]) as $tools
117 | {
118 project: $project,
119 session: $session,
120 path: $path,
121 cwd: ([$in[] | .cwd // empty] | first // null),
122 branches: ([$in[] | .gitBranch // empty | select(. != "" and . != "HEAD")] | unique),
123 first_activity: ($in | map(.timestamp) | min),
124 last_activity: ($in | map(.timestamp) | max),
125 entries_in_window: ($in | length),
126 prompt_count: ($prompts | length),
127 prompts: [$prompts[0:5][] | .[0:300]],
128 tool_calls: ($tools | length),
129 files_edited: ([$tools[]
130 | select(.name == "Edit" or .name == "Write" or .name == "MultiEdit" or .name == "NotebookEdit")
131 | .input.file_path // .input.notebook_path // empty] | unique)
132 }
133 end
134 ' < " $session_file " >> " $DIGESTS " || echo "find-sessions: skipped unreadable session ${ session_file }" >&2
135 done < <( find " $PROJECTS_DIR " -mindepth 2 -maxdepth 2 -type f -name '*.jsonl' -newer " $REF_FILE " -print0 )
136
137 jq -s 'sort_by(.first_activity)' " $DIGESTS "