Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 17 additions & 13 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,25 @@ description: |
用 cc-session CLI 讀取過去的 Claude Code session,取代直接讀 JSONL。
CLI 在 context 外完成過濾,原始 300K 壓到 30-50K,只保留對話和 tool call 一行摘要。
使用者想回顧、引用、分析過去的對話時使用。
argument-hint: "[list | inherit <id> | read <id> | context <id> | expand <id> <tool-id> | stats <id> | audit <id> | usage]"
allowed-tools:
- Bash
- Read
---

## 路由

根據 `$ARGUMENTS` 決定執行什麼。`$ARGUMENTS` 是使用者在 `/cc-session` 後面輸入的內容。

| `$ARGUMENTS` | 執行 |
|------|------|
| 空白 | 跑 `cc-session list`,把清單呈現給使用者,問想看哪個 session |
| `list`(可帶 `-p`、`-n`) | 跑 `cc-session $ARGUMENTS`,呈現清單 |
| 一個裸 session id(沒帶子命令,例如 `16d06326`) | 視為要讀這個 session → 走下方 inherit 分頁流程 |
| 已知子命令 + 參數(`inherit`/`read`/`context`/`expand`/`stats`/`audit`/`usage`) | 跑 `cc-session $ARGUMENTS`;若是 inherit 走分頁流程 |

`argument-hint` frontmatter 是輸入框看到的子命令提示,由 `cc-session help --argument-hint` 產生,安裝時自動同步,不要手改。

## 讀取 session 內容

read 預設截斷在 200 行——大多數 session 遠超這個長度,只看得到開頭一小段。
Expand All @@ -29,19 +43,9 @@ inherit 記住讀取進度,重複呼叫同一個命令即自動翻頁:

## 子命令速查

| 意圖 | 命令 |
|------|------|
| 找目標 session | `cc-session list` — 列出最近 session,`-p` 過濾專案,用過 cc-session 的標 `[refs]` |
| 讀 session(預設) | `cc-session inherit <id>` — 分頁載入,重複呼叫翻頁 |
| 查特定片段 | `cc-session read <id>` — 預設 200 行,`-offset` 跳讀 |
| 緊湊單次輸出 | `cc-session context <id>` — 同 read 但更緊湊,帶 metadata header |
| 展開單一 tool call | `cc-session expand <id> <tool-id>` — tool-id 取自輸出中的 `[Tool#xxxx]` |
| 展開同類所有 tool call | `cc-session read <id> -verbose-bash` — 也有 `-verbose-agents` / `-verbose-thinking` |
| 分析 token 消耗 | `cc-session stats <id>` |
| 檢查過濾遺漏 | `cc-session audit <id>` |
| 查看 CLI 使用紀錄 | `cc-session usage` |

Session ID 支援 prefix match,前 8 碼通常就夠。各子命令的 flags 用 `-h` 查看。
以下由 CLI 即時產生,永遠對應目前安裝的版本:

!`cc-session help`

## 輸出行為

Expand Down
136 changes: 136 additions & 0 deletions cmd/cc-session/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package main

import (
"fmt"
"os"

"github.com/Mapleeeeeeeeeee/cc-session-reader/internal/claudecodec"
)

// command describes one cc-session subcommand. It is the single source of
// truth for dispatch (main's switch), printUsage, and --argument-hint —
// those used to hand-copy the command list independently and drift out of
// sync. The "cc-session help" cheat sheet in help_cmd.go also derives each
// row's "cc-session <name> <argHint>" fragment from this registry by name,
// but keeps its intent text and teaching asides hand-written since those
// aren't part of the registry.
type command struct {
// name is the word typed after "cc-session" (os.Args[1]) and the label
// shown in printUsage / help.
name string
// summary is the one-line Chinese description shown next to name in
// printUsage's "Commands:" block.
summary string
// argHint is the input-box fragment shown in "cc-session help
// --argument-hint", e.g. "read <id>". Empty means the command is left out
// of that line (it still appears in printUsage/help unless hidden) —
// used for meta commands like "help" and "benchmark" that aren't part of
// the skill's quick-launch surface.
argHint string
// hidden excludes the command from printUsage, the argument-hint line,
// and the help cheat sheet, while keeping it dispatchable. Used for the
// deprecated "inject" alias. The cheat sheet enforces this by panicking
// if a row ever references a hidden command's name.
hidden 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
// actually needs.
run func(args []string, reader claudecodec.Codec)
}

// commands is the command registry, in the order printUsage lists them.
//
// It is populated from init() rather than the var initializer itself: the
// "help" entry's run closure calls into buildArgumentHint, which reads this
// same var, and Go's package-init cycle detector treats that as a cycle when
// it's part of the var's initializer expression. Assigning inside init()
// keeps the declaration a plain nil-slice zero value, so there is nothing
// for the cycle check to trip on.
var commands []command

func init() {
commands = []command{
{
name: "list",
summary: "列出最近的 session",
argHint: "list",
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: "read",
summary: "完整對話 + tool call 一行摘要",
argHint: "read <id>",
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: "expand",
summary: "展開特定 tool call 完整內容",
argHint: "expand <id> <tool-id>",
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: "audit",
summary: "檢視被過濾的內容取樣",
argHint: "audit <id>",
run: func(args []string, reader claudecodec.Codec) { cmdAudit(args, reader) },
},
{
name: "usage",
summary: "CLI 使用紀錄",
argHint: "usage",
run: func(args []string, _ claudecodec.Codec) { cmdUsage(args) },
},
{
name: "help",
summary: "顯示子命令速查表",
// No argHint: help is a meta/discovery command, not part of the
// skill's quick-launch surface (see buildArgumentHint in help_cmd.go).
run: func(args []string, _ claudecodec.Codec) { cmdHelp(args) },
},
{
name: "benchmark",
summary: "掃描近期 session,計算壓縮率與成本比較",
// 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,
run: func(args []string, reader claudecodec.Codec) {
fmt.Fprintln(os.Stderr, "cc-session inject 已改名為 cc-session inherit,inject 別名將於未來版本移除,請改用 inherit。")
cmdInherit(args, reader)
},
},
}
}

// findCommand looks up a command by its exact name, including hidden ones —
// hidden only affects display surfaces, not dispatch.
func findCommand(name string) (command, bool) {
for _, cmd := range commands {
if cmd.name == name {
return cmd, true
}
}
return command{}, false
}
112 changes: 112 additions & 0 deletions cmd/cc-session/help_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package main

import (
"flag"
"fmt"
"io"
"os"
"strings"
)

// cheatSheetRow is one row of the "intent -> command" quick reference
// printed by "cc-session help". intent and note are hand-written teaching
// text that has no home in the command registry; the "cc-session <name>
// <argHint>" portion is derived from the registry via commandText, so
// renaming a command or changing its argHint keeps this cheat sheet in sync
// automatically.
type cheatSheetRow struct {
// intent is the plain-language "what do I want to do" column.
intent string
// cmdName looks up the row's registry entry by command.name to build
// "cc-session <name> <argHint>". Empty for the one row that documents a
// flag variant of an existing command (read -verbose-bash) rather than a
// standalone registry entry; that row sets override instead.
cmdName string
// note is hand-written text appended after " — ", e.g. "-p 過濾專案".
// Empty when the row has no aside.
note string
// override replaces the registry-derived command string outright. Only
// the "-verbose-bash" row uses this, since it isn't a standalone
// registry entry.
override string
}

// commandText renders the row's "cc-session ..." command string, followed by
// " — note" when note is set.
func (r cheatSheetRow) commandText() string {
command := r.override
if command == "" {
cmd, ok := findCommand(r.cmdName)
if !ok {
panic("cheatSheet: unknown command name " + r.cmdName)
}
// hidden commands (e.g. the deprecated "inject" alias) must never
// surface in the help cheat sheet; see the hidden field's doc
// comment in commands.go.
if cmd.hidden {
panic("cheatSheet: refuses to surface hidden command " + r.cmdName)
}
command = "cc-session " + cmd.argHint
}
if r.note == "" {
return command
}
return command + " — " + r.note
}

var cheatSheet = []cheatSheetRow{
{intent: "找目標 session", cmdName: "list", note: "列出最近 session,-p 過濾專案,用過 cc-session 的標 [refs]"},
{intent: "讀 session(預設)", cmdName: "inherit", note: "分頁載入,重複呼叫翻頁"},
{intent: "查特定片段", cmdName: "read", note: "預設 200 行,-offset 跳讀"},
{intent: "緊湊單次輸出", cmdName: "context", note: "同 read 但更緊湊,帶 metadata header"},
{intent: "展開單一 tool call", cmdName: "expand", note: "tool-id 取自輸出中的 [Tool#xxxx]"},
{intent: "展開同類所有 tool call", override: "cc-session read <id> -verbose-bash", note: "也有 -verbose-agents / -verbose-thinking"},
{intent: "分析 token 消耗", cmdName: "stats"},
{intent: "檢查過濾遺漏", cmdName: "audit"},
{intent: "查看 CLI 使用紀錄", cmdName: "usage"},
}

func cmdHelp(args []string) {
exitOnError(runHelp(args, os.Stdout, os.Stderr))
}

func runHelp(args []string, out io.Writer, errOut io.Writer) error {
fs := flag.NewFlagSet("help", flag.ContinueOnError)
fs.SetOutput(errOut)
isArgumentHint := fs.Bool("argument-hint", false, "print a single-line [cmd | cmd ...] hint for a skill's argument-hint frontmatter")
if err := fs.Parse(args); err != nil {
return err
}

if *isArgumentHint {
fmt.Fprintln(out, buildArgumentHint())
return nil
}

// Rows are printed as "意圖 → 命令" rather than aligned columns: Go's
// text/tabwriter pads by rune count, but CJK characters render two
// columns wide in a terminal, so column-aligned output goes ragged.
fmt.Fprintln(out, "cc-session 子命令速查表")
fmt.Fprintln(out)
for _, row := range cheatSheet {
fmt.Fprintf(out, "%s → %s\n", row.intent, row.commandText())
}
fmt.Fprintln(out)
fmt.Fprintln(out, "Session ID 支援 prefix match,前 8 碼通常就夠。各子命令的 flags 用 -h 查看。")
return nil
}

// buildArgumentHint renders the registry's non-hidden, hinted commands as a
// single "[a | b | c]" line for a Claude Code skill's argument-hint
// frontmatter. Commands with an empty argHint (e.g. "help", "benchmark") and
// hidden commands (e.g. "inject") are left out.
func buildArgumentHint() string {
var hints []string
for _, cmd := range commands {
if cmd.hidden || cmd.argHint == "" {
continue
}
hints = append(hints, cmd.argHint)
}
return "[" + strings.Join(hints, " | ") + "]"
}
69 changes: 33 additions & 36 deletions cmd/cc-session/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Package main is the CLI entry point for the Claude session reader.
// Subcommands: list, read, context, stats, audit, expand, usage, inherit.
// "inject" is kept as a hidden, deprecated alias for "inherit".
// Run "cc-session help" for a usage cheat sheet, or see the command
// registry in commands.go for the authoritative subcommand list.
package main

import (
Expand Down Expand Up @@ -43,53 +43,50 @@ func main() {

subcommand := os.Args[1]
switch subcommand {
case "-h", "--help", "help":
case "-h", "--help":
printUsage()
return
case "-v", "--version", "version":
fmt.Printf("cc-session %s\n", version)
return
case "list":
cmdList(os.Args[2:], reader)
case "read":
cmdRead(os.Args[2:], reader)
case "context":
cmdContext(os.Args[2:], reader)
case "stats":
cmdStats(os.Args[2:], reader)
case "audit":
cmdAudit(os.Args[2:], reader)
case "expand":
cmdExpand(os.Args[2:], reader)
case "usage":
cmdUsage(os.Args[2:])
case "inherit":
cmdInherit(os.Args[2:], reader)
case "inject":
fmt.Fprintln(os.Stderr, "cc-session inject 已改名為 cc-session inherit,inject 別名將於未來版本移除,請改用 inherit。")
cmdInherit(os.Args[2:], reader)
case "benchmark":
cmdBenchmark(os.Args[2:], reader)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", subcommand)
printUsage()
os.Exit(1)
cmd, ok := findCommand(subcommand)
if !ok {
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", subcommand)
printUsage()
os.Exit(1)
}
cmd.run(os.Args[2:], reader)
}
}

func printUsage() {
fmt.Fprintln(os.Stderr, "Usage: cc-session <command> [options]")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Commands:")
fmt.Fprintln(os.Stderr, " list 列出最近的 session")
fmt.Fprintln(os.Stderr, " read 完整對話 + tool call 一行摘要")
fmt.Fprintln(os.Stderr, " context 精簡注入格式(帶 metadata header)")
fmt.Fprintln(os.Stderr, " stats 字元與 token 分佈統計")
fmt.Fprintln(os.Stderr, " audit 檢視被過濾的內容取樣")
fmt.Fprintln(os.Stderr, " expand 展開特定 tool call 完整內容")
fmt.Fprintln(os.Stderr, " usage CLI 使用紀錄")
fmt.Fprintln(os.Stderr, " inherit 分頁繼承 session 到 context")
fmt.Fprintln(os.Stderr, " benchmark 掃描近期 session,計算壓縮率與成本比較")
nameFormat := fmt.Sprintf(" %%-%ds %%s\n", longestVisibleCommandNameLen())
for _, cmd := range commands {
if cmd.hidden {
continue
}
fmt.Fprintf(os.Stderr, nameFormat, cmd.name, cmd.summary)
}
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Run 'cc-session <command> -h' for command-specific flags.")
}

// longestVisibleCommandNameLen returns the length of the longest non-hidden
// command name, so printUsage can pad its name column wide enough for every
// entry (a fixed width goes ragged once a name like "benchmark" exceeds it).
func longestVisibleCommandNameLen() int {
maxLen := 0
for _, cmd := range commands {
if cmd.hidden {
continue
}
if len(cmd.name) > maxLen {
maxLen = len(cmd.name)
}
}
return maxLen
}
Loading
Loading