@tznthou/ccrecall 0.6.0 → 0.7.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/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +5 -1
- package/dist/api/routes.js.map +1 -1
- package/dist/core/database.d.ts +15 -0
- package/dist/core/database.d.ts.map +1 -1
- package/dist/core/database.js +54 -0
- package/dist/core/database.js.map +1 -1
- package/dist/core/project-id.d.ts +19 -0
- package/dist/core/project-id.d.ts.map +1 -0
- package/dist/core/project-id.js +24 -0
- package/dist/core/project-id.js.map +1 -0
- package/dist/core/topic-extractor.d.ts +4 -0
- package/dist/core/topic-extractor.d.ts.map +1 -1
- package/dist/core/topic-extractor.js +110 -2
- package/dist/core/topic-extractor.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +38 -9
- package/dist/mcp/tools.js.map +1 -1
- package/hooks/lib/project-id.mjs +22 -0
- package/hooks/session-start.mjs +6 -4
- package/hooks/user-prompt-submit.mjs +6 -4
- package/package.json +1 -1
- package/scripts/backfill-memory-topics.ts +65 -8
- package/scripts/post-session-extract.sh +111 -12
- package/scripts/usefulness-report.sql +27 -2
|
@@ -15,6 +15,71 @@
|
|
|
15
15
|
CCRECALL_PORT="${CCRECALL_PORT:-7749}"
|
|
16
16
|
CCRECALL_EXTRACT_LOG="${CCRECALL_EXTRACT_LOG:-$HOME/.ccrecall/extract.log.jsonl}"
|
|
17
17
|
|
|
18
|
+
# Telemetry rows carry $PWD (added with #89), which discloses the account name
|
|
19
|
+
# and every project name the user works on. A log first created under a
|
|
20
|
+
# permissive umask lands at 0644 — world-readable on a shared machine. Both
|
|
21
|
+
# write paths below go through here: file permissions belong to the file, not
|
|
22
|
+
# to the call site, so securing only one of them secures neither — whichever
|
|
23
|
+
# path runs first decides the mode.
|
|
24
|
+
#
|
|
25
|
+
# Returns non-zero if the log cannot be secured, and callers then skip the
|
|
26
|
+
# write: losing a telemetry row is cheaper than leaking paths.
|
|
27
|
+
#
|
|
28
|
+
# Every step is silenced, the creation included: a shell redirection error
|
|
29
|
+
# names the full path on stderr, which is the account and project name this
|
|
30
|
+
# function exists to withhold. Failing quietly is the contract — the caller
|
|
31
|
+
# decides what, if anything, to say.
|
|
32
|
+
_ccrecall_secure_log() {
|
|
33
|
+
local f="$1"
|
|
34
|
+
# Created under the restrictive umask so a directory this line brings into
|
|
35
|
+
# existence starts at 0700. It deliberately does not touch the mode of a
|
|
36
|
+
# directory that is already there: CCRECALL_EXTRACT_LOG is user-configurable,
|
|
37
|
+
# so that directory can be one the user picked — a project root, or the
|
|
38
|
+
# current directory for a relative setting — and neither tightening it nor
|
|
39
|
+
# refusing to use it is this script's call to make. Both were tried and both
|
|
40
|
+
# were worse: chmod turned a project directory 0755 into 0700, and refusing
|
|
41
|
+
# dropped every row forever on a `umask 002` machine, silently.
|
|
42
|
+
#
|
|
43
|
+
# Existing installs therefore keep whatever mode they have. ~/.ccrecall is
|
|
44
|
+
# also created on the TypeScript side without a mode (src/core/database.ts,
|
|
45
|
+
# src/core/integrity-monitor.ts; only recall-telemetry.ts passes 0o700), and
|
|
46
|
+
# whichever creator runs first wins. Fixing that belongs there, not here.
|
|
47
|
+
(umask 077; mkdir -p "$(dirname "$f")") 2>/dev/null || return 1
|
|
48
|
+
# Reject a symlink. This narrows the window rather than closing it: the test
|
|
49
|
+
# here, the chmod below and the append that follows are separate pathname
|
|
50
|
+
# resolutions, and both chmod(2) and >> follow links, so an attacker who can
|
|
51
|
+
# write this directory can still win the race by planting between them.
|
|
52
|
+
# Closing it properly needs open(O_NOFOLLOW|O_APPEND) + fchmod on the one
|
|
53
|
+
# descriptor, which shell cannot express; the real remedy is the directory
|
|
54
|
+
# not being writable by anyone else in the first place.
|
|
55
|
+
if [ -L "$f" ]; then return 1; fi
|
|
56
|
+
[ -e "$f" ] || (umask 077; : >> "$f") 2>/dev/null || return 1
|
|
57
|
+
# And refuse anything that is not a regular file: chmod on a directory
|
|
58
|
+
# succeeds and leaves it non-traversable, and the append then fails open.
|
|
59
|
+
[ -f "$f" ] || return 1
|
|
60
|
+
chmod 600 "$f" 2>/dev/null || return 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# The only place that appends to the telemetry log. Both callers go through
|
|
64
|
+
# here rather than each pairing a write with its own guard: a guard the caller
|
|
65
|
+
# has to remember is one a later caller forgets, and permissions belong to the
|
|
66
|
+
# file — a single unguarded append under a permissive umask re-creates the log
|
|
67
|
+
# 0644 and every other writer inherits it. Securing is not the caller's job to
|
|
68
|
+
# get right, so it is not the caller's job at all.
|
|
69
|
+
#
|
|
70
|
+
# Arguments after the path are forwarded verbatim to `jq -n -c`.
|
|
71
|
+
# Silenced like every step of the guard: the redirection is set up by the
|
|
72
|
+
# shell, so an open that fails here — the target replaced between the guard
|
|
73
|
+
# and this line, a full disk — prints the full path, the same disclosure the
|
|
74
|
+
# mode exists to prevent. jq's own diagnostics go with it; its arguments are
|
|
75
|
+
# fixed in this file rather than user input, so a jq error is a bug this
|
|
76
|
+
# repo's tests catch, not a condition to report at runtime.
|
|
77
|
+
_ccrecall_log_append() {
|
|
78
|
+
local f="$1"; shift
|
|
79
|
+
_ccrecall_secure_log "$f" || return 1
|
|
80
|
+
{ jq -n -c "$@" >> "$f"; } 2>/dev/null
|
|
81
|
+
}
|
|
82
|
+
|
|
18
83
|
# Resolve the directory containing this script (for prompt file).
|
|
19
84
|
# zsh leaves BASH_SOURCE empty (the user's shell is zsh), so detect zsh and
|
|
20
85
|
# use its %x prompt path; eval isolates the zsh-only ${(%):-%x} syntax from
|
|
@@ -29,10 +94,19 @@ CCRECALL_SCRIPT_DIR="$(cd "$(dirname "$_ccrecall_src")" && pwd)"
|
|
|
29
94
|
unset _ccrecall_src
|
|
30
95
|
|
|
31
96
|
ccrecall-extract() {
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
97
|
+
# #89: this used to derive project_id here with `sed 's|/|-|g'`, which
|
|
98
|
+
# diverged from Claude Code's char-wise [^A-Za-z0-9] encoding for any cwd
|
|
99
|
+
# holding a space, dot, underscore or CJK — and every such session then
|
|
100
|
+
# skipped extraction forever with no error surfaced.
|
|
101
|
+
#
|
|
102
|
+
# We do NOT reimplement the encoding in shell. BSD sed/tr are byte-wise and
|
|
103
|
+
# emit three dashes per CJK character; getting it right here would mean a
|
|
104
|
+
# second implementation to keep in sync. Instead /session/last returns the
|
|
105
|
+
# id the daemon read off disk (see routes.ts) and we use that verbatim.
|
|
106
|
+
# It stays empty until that call lands, which is fine: every consumer of
|
|
107
|
+
# project_id below sits behind a valid session_id, which implies the call
|
|
108
|
+
# succeeded.
|
|
109
|
+
local project_id=""
|
|
36
110
|
|
|
37
111
|
# ── Trust pre-flight ──
|
|
38
112
|
# ccrecall-extract always launches claude with --dangerously-skip-permissions, which
|
|
@@ -83,6 +157,9 @@ ccrecall-extract() {
|
|
|
83
157
|
# skipping extraction for ~1 in 5 sessions (any title with a newline,
|
|
84
158
|
# e.g. every cmux "/model" opener). Root-caused 2026-07-16.
|
|
85
159
|
session_id=$(printf '%s' "$session_meta" | jq -r '.sessionId // empty' 2>/dev/null)
|
|
160
|
+
# #89: authoritative project id, read off the ~/.claude/projects/ directory
|
|
161
|
+
# name by the indexer rather than re-derived from $PWD here.
|
|
162
|
+
project_id=$(printf '%s' "$session_meta" | jq -r '.projectId // empty' 2>/dev/null)
|
|
86
163
|
fi
|
|
87
164
|
|
|
88
165
|
# Load the structured prompt
|
|
@@ -170,14 +247,17 @@ ${prompt}"
|
|
|
170
247
|
# more and usually saved nothing). A missed session is cheaper than that.
|
|
171
248
|
if [[ "$transcript_mode" != "jsonl" ]]; then
|
|
172
249
|
printf '\n⚠️ ccRecall: no text transcript (%s) — skipping extraction.\n' "$skip_reason"
|
|
173
|
-
|
|
174
|
-
|
|
250
|
+
# cwd, not just projectId: on a skip the daemon lookup may never have
|
|
251
|
+
# landed, leaving projectId empty. The raw cwd is the one fact always
|
|
252
|
+
# available, and #89 was diagnosed precisely by comparing it against the
|
|
253
|
+
# directory names under ~/.claude/projects/.
|
|
254
|
+
_ccrecall_log_append "$CCRECALL_EXTRACT_LOG" \
|
|
175
255
|
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
176
256
|
--arg sid "$session_id" \
|
|
177
257
|
--arg pid "$project_id" \
|
|
258
|
+
--arg cwd "$PWD" \
|
|
178
259
|
--arg reason "$skip_reason" \
|
|
179
|
-
'{ts:$ts,sessionId:$sid,projectId:$pid,mode:"skip",reason:$reason,exitCode:null,durationSec:0}'
|
|
180
|
-
>> "$CCRECALL_EXTRACT_LOG"
|
|
260
|
+
'{ts:$ts,sessionId:$sid,projectId:$pid,cwd:$cwd,mode:"skip",reason:$reason,exitCode:null,durationSec:0}' || :
|
|
181
261
|
return $claude_exit
|
|
182
262
|
fi
|
|
183
263
|
|
|
@@ -223,7 +303,18 @@ ${prompt}"
|
|
|
223
303
|
# command before it).
|
|
224
304
|
local log_dir
|
|
225
305
|
log_dir=$(dirname "$CCRECALL_EXTRACT_LOG")
|
|
226
|
-
|
|
306
|
+
# Same umask as the log guard uses. This line runs ~95 lines before the
|
|
307
|
+
# telemetry append and creates the directory on every non-skip session, so
|
|
308
|
+
# without it the guard's own mkdir is a no-op here and the two paths through
|
|
309
|
+
# this function would leave the directory at different modes — 0700 when a
|
|
310
|
+
# skip returns early, 0755 otherwise.
|
|
311
|
+
#
|
|
312
|
+
# Silenced and non-fatal for the same reasons as everything else that
|
|
313
|
+
# touches this path: a failure prints $log_dir, and a bare command is not
|
|
314
|
+
# exempt from errexit. Ignoring the failure is safe — mktemp below degrades
|
|
315
|
+
# to /dev/null on its own, and the log guard re-creates the directory when
|
|
316
|
+
# the telemetry row is written.
|
|
317
|
+
(umask 077; mkdir -p "$log_dir") 2>/dev/null || :
|
|
227
318
|
find "$log_dir" -maxdepth 1 -name 'extract-stdout.*' -mmin +60 -delete 2>/dev/null
|
|
228
319
|
# Trailing Xs are mandatory: BSD/macOS mktemp only substitutes Xs at the END
|
|
229
320
|
# of the template — an inner XXXXXX (e.g. a .tmp suffix after it) is taken
|
|
@@ -309,7 +400,16 @@ ${prompt}"
|
|
|
309
400
|
# rather than invoking — splits exit-0 zero-write rows into silent miss vs
|
|
310
401
|
# genuine no-op (#75). Absent on rows written before this marker shipped, so
|
|
311
402
|
# analysis must treat missing as unknown, not as 0.
|
|
312
|
-
|
|
403
|
+
# Secured even though this row carries no cwd: the mode belongs to the file,
|
|
404
|
+
# and this path can be the one that creates it.
|
|
405
|
+
#
|
|
406
|
+
# `|| :` on both call sites is load-bearing under `set -e` / `setopt
|
|
407
|
+
# err_exit`. The previous shape put the guard in an `if` condition, which
|
|
408
|
+
# errexit exempts; a bare command is not exempt, so a dropped telemetry row
|
|
409
|
+
# would abort the function before `return $claude_exit` and hand the caller
|
|
410
|
+
# 1 instead of Claude's real exit status — in an interactive shell with
|
|
411
|
+
# errexit set, it would close the shell.
|
|
412
|
+
_ccrecall_log_append "$CCRECALL_EXTRACT_LOG" \
|
|
313
413
|
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
314
414
|
--arg sid "$session_id" \
|
|
315
415
|
--arg pid "$project_id" \
|
|
@@ -318,8 +418,7 @@ ${prompt}"
|
|
|
318
418
|
--argjson exit "$extract_exit" \
|
|
319
419
|
--argjson dur "$extract_duration" \
|
|
320
420
|
--argjson textSaves "$recall_save_text_count" \
|
|
321
|
-
'{ts:$ts,sessionId:$sid,projectId:$pid,mode:"jsonl",exitCode:$exit,durationSec:$dur,stderr:$stderr,stdoutMarker:$marker,recallSaveTextCount:$textSaves}'
|
|
322
|
-
>> "$CCRECALL_EXTRACT_LOG"
|
|
421
|
+
'{ts:$ts,sessionId:$sid,projectId:$pid,mode:"jsonl",exitCode:$exit,durationSec:$dur,stderr:$stderr,stdoutMarker:$marker,recallSaveTextCount:$textSaves}' || :
|
|
323
422
|
|
|
324
423
|
return $claude_exit
|
|
325
424
|
}
|
|
@@ -28,16 +28,41 @@ SELECT
|
|
|
28
28
|
FROM injection_log;
|
|
29
29
|
|
|
30
30
|
-- ── Coverage ───────────────────────────────────────────────────────
|
|
31
|
+
-- Broken out per source. The done-criteria count *startup* injections, and
|
|
32
|
+
-- v0.6.0 added a second writer to this table (`prompt` — mid-conversation
|
|
33
|
+
-- recall), so an unfiltered COUNT(DISTINCT memory_id) mixes the two. Read the
|
|
34
|
+
-- startup row. The rows do not sum: a memory injected by two sources is
|
|
35
|
+
-- distinct within each.
|
|
31
36
|
|
|
32
|
-
SELECT '── COVERAGE ──' AS '';
|
|
37
|
+
SELECT '── COVERAGE BY SOURCE (lifetime) ──' AS '';
|
|
33
38
|
|
|
34
39
|
SELECT
|
|
40
|
+
source,
|
|
35
41
|
MIN(injected_at) AS first,
|
|
36
42
|
MAX(injected_at) AS last,
|
|
37
43
|
CAST(julianday(MAX(injected_at)) - julianday(MIN(injected_at)) AS INTEGER) AS span_days,
|
|
44
|
+
COUNT(*) AS injections,
|
|
38
45
|
COUNT(DISTINCT memory_id) AS distinct_memories,
|
|
39
46
|
COUNT(DISTINCT session_id) AS distinct_sessions
|
|
40
|
-
FROM injection_log
|
|
47
|
+
FROM injection_log
|
|
48
|
+
GROUP BY source
|
|
49
|
+
ORDER BY injections DESC;
|
|
50
|
+
|
|
51
|
+
-- Criterion 2 is scoped to a 7-day window; the lifetime figures above span the
|
|
52
|
+
-- whole table and read far higher. Rolling window rather than hardcoded dates,
|
|
53
|
+
-- so the report stays usable past the verdict.
|
|
54
|
+
|
|
55
|
+
SELECT '── COVERAGE BY SOURCE (last 7 days) ──' AS '';
|
|
56
|
+
|
|
57
|
+
SELECT
|
|
58
|
+
source,
|
|
59
|
+
COUNT(*) AS injections,
|
|
60
|
+
COUNT(DISTINCT memory_id) AS distinct_memories,
|
|
61
|
+
COUNT(DISTINCT session_id) AS distinct_sessions
|
|
62
|
+
FROM injection_log
|
|
63
|
+
WHERE injected_at >= datetime('now', '-7 days')
|
|
64
|
+
GROUP BY source
|
|
65
|
+
ORDER BY injections DESC;
|
|
41
66
|
|
|
42
67
|
-- ── Overlap distribution ───────────────────────────────────────────
|
|
43
68
|
|