Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b847bf3
fix: determine tool result status via ladder instead of defaulting to ok
Mapleeeeeeeeeee Jul 15, 2026
fab62d7
feat: diff summaries for Edit/Write and graceful unknown-tool fallback
Mapleeeeeeeeeee Jul 15, 2026
40f5497
fix: emit minimal context header when session metadata is missing
Mapleeeeeeeeeee Jul 15, 2026
ffd1bee
fix: compute list duration and preview without session metadata
Mapleeeeeeeeeee Jul 15, 2026
e52c714
feat: record command outcomes in usage log and normalize inject alias
Mapleeeeeeeeeee Jul 15, 2026
3b81d2f
fix: measure stats from the real render pipeline so breakdown reconciles
Mapleeeeeeeeeee Jul 15, 2026
a582977
refactor: address review findings from /review and /test-review
Mapleeeeeeeeeee Jul 15, 2026
c26aa47
feat: audit semantic histogram and failure-length distribution
Mapleeeeeeeeeee Jul 16, 2026
b8ec5d3
docs: ADR-004 failure retention strategy — investigated, deferred
Mapleeeeeeeeeee Jul 16, 2026
153dd6a
test: pin JSONL malformed-input and UTF-8 truncation boundary contracts
Mapleeeeeeeeeee Jul 16, 2026
7d30256
feat: record requested tool ids in expand usage telemetry
Mapleeeeeeeeeee Jul 16, 2026
3f671d6
feat: collapse retry loops and consecutive same-file reads
Mapleeeeeeeeeee Jul 16, 2026
46f4c6b
docs: ADR-005 collapse rules for retry loops and same-file reads
Mapleeeeeeeeeee Jul 16, 2026
cf71ce7
fix: collapse only identical errors and require token-boundary multi-…
Mapleeeeeeeeeee Jul 16, 2026
f78aede
fix: windows-safe project dir names and CRLF-tolerant SKILL.md parsin…
Mapleeeeeeeeeee Jul 16, 2026
31ed55f
chore: gitignore local .claude directory
Mapleeeeeeeeeee Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ bin/
coverage.out
dist/
/cc-session
.claude/
27 changes: 24 additions & 3 deletions cmd/cc-session/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,34 @@ func runAudit(args []string, out io.Writer, errOut io.Writer, store parser.Store

result := analyzer.ComputeAudit(events)

for _, catName := range []string{"tool_result_cut", "system_noise", "thinking"} {
items := result.Categories[catName]
fmt.Fprintln(out, "=== CUT-content histogram (risk-descending) ===")
for _, bucket := range analyzer.BucketOrder {
items := result.Samples[bucket]
if len(items) == 0 {
continue
}
fmt.Fprintf(out, " %-24s %6s items %12s chars\n",
bucket, analyzer.FormatNumber(len(items)), analyzer.FormatNumber(result.BucketChars[bucket]))
}

fmt.Fprintln(out, "\n=== Failed tool result length (chars) ===")
if result.Failures.Count == 0 {
fmt.Fprintln(out, " no failed tool results in this session")
} else {
f := result.Failures
fmt.Fprintf(out, " count: %s median: %s p90: %s max: %s\n",
analyzer.FormatNumber(f.Count), analyzer.FormatNumber(f.Median),
analyzer.FormatNumber(f.P90), analyzer.FormatNumber(f.Max))
}

fmt.Fprintln(out, "\n=== Samples ===")
for _, bucket := range analyzer.BucketOrder {
items := result.Samples[bucket]
if len(items) == 0 {
continue
}
shown := sampleCount(*samples, len(items))
fmt.Fprintf(out, "=== %s (%d items, showing %d) ===\n", catName, len(items), shown)
fmt.Fprintf(out, "=== %s (%d items, showing %d) ===\n", bucket, len(items), shown)
for _, item := range items[:shown] {
fmt.Fprintf(out, " %s\n\n", item)
}
Expand Down
79 changes: 47 additions & 32 deletions cmd/cc-session/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ type command struct {
// deprecated "inject" alias. The cheat sheet enforces this by panicking
// if a row ever references a hidden command's name.
hidden bool
// tracksUsage marks commands whose invocations should be recorded to
// usage.jsonl. main's dispatch starts tracking (see beginUsageTracking
// in usage_cmd.go) before run executes, so a failure anywhere inside
// run — including before it resolves a session ID — still produces a
// usage entry. Meta commands ("help", "usage" itself) leave this false.
tracksUsage bool
// run executes the command. reader is the concrete claudecodec.Codec,
// which satisfies both session.TranscriptReader and session.HeaderScanner
// so one signature covers every cmdXxx regardless of which interface it
Expand All @@ -52,46 +58,53 @@ var commands []command
func init() {
commands = []command{
{
name: "list",
summary: "列出最近的 session",
argHint: "list",
run: func(args []string, reader claudecodec.Codec) { cmdList(args, reader) },
name: "list",
summary: "列出最近的 session",
argHint: "list",
tracksUsage: true,
run: func(args []string, reader claudecodec.Codec) { cmdList(args, reader) },
},
{
name: "inherit",
summary: "分頁繼承 session 到 context",
argHint: "inherit <id>",
run: func(args []string, reader claudecodec.Codec) { cmdInherit(args, reader) },
name: "inherit",
summary: "分頁繼承 session 到 context",
argHint: "inherit <id>",
tracksUsage: true,
run: func(args []string, reader claudecodec.Codec) { cmdInherit(args, reader) },
},
{
name: "read",
summary: "完整對話 + tool call 一行摘要",
argHint: "read <id>",
run: func(args []string, reader claudecodec.Codec) { cmdRead(args, reader) },
name: "read",
summary: "完整對話 + tool call 一行摘要",
argHint: "read <id>",
tracksUsage: true,
run: func(args []string, reader claudecodec.Codec) { cmdRead(args, reader) },
},
{
name: "context",
summary: "精簡注入格式(帶 metadata header)",
argHint: "context <id>",
run: func(args []string, reader claudecodec.Codec) { cmdContext(args, reader) },
name: "context",
summary: "精簡注入格式(帶 metadata header)",
argHint: "context <id>",
tracksUsage: true,
run: func(args []string, reader claudecodec.Codec) { cmdContext(args, reader) },
},
{
name: "expand",
summary: "展開特定 tool call 完整內容",
argHint: "expand <id> <tool-id>",
run: func(args []string, reader claudecodec.Codec) { cmdExpand(args, reader) },
name: "expand",
summary: "展開特定 tool call 完整內容",
argHint: "expand <id> <tool-id>",
tracksUsage: true,
run: func(args []string, reader claudecodec.Codec) { cmdExpand(args, reader) },
},
{
name: "stats",
summary: "字元與 token 分佈統計",
argHint: "stats <id>",
run: func(args []string, reader claudecodec.Codec) { cmdStats(args, reader) },
name: "stats",
summary: "字元與 token 分佈統計",
argHint: "stats <id>",
tracksUsage: true,
run: func(args []string, reader claudecodec.Codec) { cmdStats(args, reader) },
},
{
name: "audit",
summary: "檢視被過濾的內容取樣",
argHint: "audit <id>",
run: func(args []string, reader claudecodec.Codec) { cmdAudit(args, reader) },
name: "audit",
summary: "檢視被過濾的內容取樣",
argHint: "audit <id>",
tracksUsage: true,
run: func(args []string, reader claudecodec.Codec) { cmdAudit(args, reader) },
},
{
name: "usage",
Expand All @@ -107,15 +120,17 @@ func init() {
run: func(args []string, _ claudecodec.Codec) { cmdHelp(args) },
},
{
name: "benchmark",
summary: "掃描近期 session,計算壓縮率與成本比較",
name: "benchmark",
summary: "掃描近期 session,計算壓縮率與成本比較",
tracksUsage: true,
// No argHint: benchmark is a maintainer/analysis command, not part of
// the skill's quick-launch surface.
run: func(args []string, reader claudecodec.Codec) { cmdBenchmark(args, reader) },
},
{
name: "inject",
hidden: true,
name: "inject",
hidden: true,
tracksUsage: true,
run: func(args []string, reader claudecodec.Codec) {
fmt.Fprintln(os.Stderr, "cc-session inject 已改名為 cc-session inherit,inject 別名將於未來版本移除,請改用 inherit。")
cmdInherit(args, reader)
Expand Down
2 changes: 1 addition & 1 deletion cmd/cc-session/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func TestCLI_WhenSessionExists_ThenListReadContextAndAuditWorkEndToEnd(t *testin
name: "audit samples cut tool results",
args: []string{"audit", sid, "-n", "5"},
want: []string{
"=== tool_result_cut (1 items, showing 1) ===",
"=== success_output_bash (1 items, showing 1) ===",
},
},
{
Expand Down
2 changes: 1 addition & 1 deletion cmd/cc-session/expand.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func runExpand(args []string, out io.Writer, errOut io.Writer, store parser.Stor
if resolved.Path == "" {
return fmt.Errorf("transcript not found: %s", resolved.ID)
}
logUsageAsync("expand", session.ShortID(resolved.ID, 8))
logUsageAsync("expand", session.ShortID(resolved.ID, 8), requestedIDs...)

events, err := reader.ReadAll(resolved.Path)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cmd/cc-session/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
)

func exitOnError(err error) {
finalizeUsageLog(err)
if err == nil {
return
}
Expand Down
40 changes: 40 additions & 0 deletions cmd/cc-session/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package main
import (
"fmt"
"os"
"runtime/debug"

"github.com/Mapleeeeeeeeeee/cc-session-reader/internal/claudecodec"
"github.com/Mapleeeeeeeeeee/cc-session-reader/internal/tokens"
Expand All @@ -14,6 +15,40 @@ import (
var version = "dev"
var commit = "none"

// readBuildInfo is the debug.ReadBuildInfo backend used by resolveVersion. It
// is a package-level seam so tests can substitute deterministic build info
// instead of whatever the go test binary itself was built with.
var readBuildInfo = debug.ReadBuildInfo

// resolveVersion returns version unchanged unless it's still the "dev"
// placeholder, meaning goreleaser's ldflags -X override (see .goreleaser.yaml)
// never ran — a plain `go build`/`go install` of this module. In that case it
// falls back to runtime/debug build info: `go install pkg@version` embeds the
// module version in info.Main.Version, while a source build embeds the VCS
// revision in info.Settings. Returns "dev" unchanged if neither is available.
func resolveVersion(version string) string {
if version != "dev" {
return version
}
info, ok := readBuildInfo()
if !ok {
return version
}
if info.Main.Version != "" && info.Main.Version != "(devel)" {
return info.Main.Version
}
for _, s := range info.Settings {
if s.Key == "vcs.revision" && s.Value != "" {
rev := s.Value
if len(rev) > 12 {
rev = rev[:12]
}
return "dev+" + rev
}
}
return version
}

type countTokensFunc func(string) (int, error)

// countTokensFn is the token-counting backend used by runStats. It is a
Expand All @@ -37,6 +72,8 @@ func main() {
os.Exit(1)
}

version = resolveVersion(version)

defer waitUsageLog()

reader := claudecodec.Codec{}
Expand All @@ -56,6 +93,9 @@ func main() {
printUsage()
os.Exit(1)
}
if cmd.tracksUsage {
beginUsageTracking(cmd.name)
}
cmd.run(os.Args[2:], reader)
}
}
Expand Down
Loading
Loading