1
0
Fork 0
mirror of https://github.com/SeriousBug/dotfiles synced 2026-06-16 20:35:08 -05:00
dotfiles/bin/claude-token-count
Kaan Barmore-Genc b0070920e3 Add cc-compact skill and claude-token-count CLI
- cc-compact: skill to reload a past Claude Code session's context
  (resolves by id or ai-title, extracts a bounded XML summary via
  compact_session.py) so work can be resumed without ingesting the log
- migrate the existing new-work skill into the repo
- deploy ~/.claude/skills via ensure_dir_symlink in setup.sh
- claude-token-count: count tokens via Anthropic's count_tokens endpoint,
  pulling the API key from 1Password at runtime
2026-05-31 02:28:42 -05:00

96 lines
3.2 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# claude-token-count — count tokens for text using Anthropic's
# /v1/messages/count_tokens endpoint (the exact Claude tokenizer).
#
# The API key is pulled from 1Password at runtime via the `op` CLI; it is
# never written to disk and is passed to curl via a config file on /dev/fd
# so it stays out of the process argument list.
#
# Usage:
# claude-token-count "some text" # count the argument(s)
# claude-token-count --file notes.md # count a file's contents
# some-command | claude-token-count # count piped stdin
#
# Options:
# --file PATH read the text to count from PATH
# --model NAME model whose tokenizer to use (default: $CLAUDE_TOKEN_COUNT_MODEL
# or claude-sonnet-4-6)
# --raw print the raw JSON response instead of just the number
# -h, --help show this help
#
# Requires: op (1Password CLI, signed in), curl, jq.
set -euo pipefail
# 1Password reference for the Anthropic API key (from the item's share link).
OP_ACCOUNT="EADC56ZW7NARRJTNZKSRBYRQNU"
OP_REF="op://m6vn2ie66x7ysrjq73q45oiyeu/te6tuanr426ohu33t2qhqzjrb4/credential"
MODEL="${CLAUDE_TOKEN_COUNT_MODEL:-claude-sonnet-4-6}"
RAW=false
FILE=""
declare -a TEXT_PARTS=()
usage() { sed -n '2,/^set -euo/{/^set -euo/!p}' "$0" | sed 's/^# \{0,1\}//'; }
while [[ $# -gt 0 ]]; do
case "$1" in
--file) FILE="${2:?--file requires a path}"; shift 2 ;;
--file=*) FILE="${1#--file=}"; shift ;;
--model) MODEL="${2:?--model requires a name}"; shift 2 ;;
--model=*) MODEL="${1#--model=}"; shift ;;
--raw) RAW=true; shift ;;
-h|--help) usage; exit 0 ;;
*) TEXT_PARTS+=("$1"); shift ;;
esac
done
for tool in op curl jq; do
command -v "$tool" >/dev/null 2>&1 || { echo "Error: '$tool' is required but not installed." >&2; exit 1; }
done
# Resolve the text to count: --file wins, then positional args, then stdin.
if [[ -n "$FILE" ]]; then
[[ -r "$FILE" ]] || { echo "Error: cannot read file: $FILE" >&2; exit 1; }
TEXT="$(cat -- "$FILE")"
elif [[ ${#TEXT_PARTS[@]} -gt 0 ]]; then
TEXT="${TEXT_PARTS[*]}"
elif [[ ! -t 0 ]]; then
TEXT="$(cat)"
else
echo "Error: no input. Provide text as an argument, --file, or via stdin." >&2
exit 2
fi
[[ -n "$TEXT" ]] || { echo "Error: input is empty." >&2; exit 2; }
# Build the request body, encoding the text safely as a JSON string.
BODY="$(jq -n --arg model "$MODEL" --arg text "$TEXT" \
'{model: $model, messages: [{role: "user", content: $text}]}')"
# Fetch the key into a variable (local to this process) and hand it to curl
# through a --config file via process substitution, keeping it out of argv.
API_KEY="$(op read --account "$OP_ACCOUNT" "$OP_REF")"
RESPONSE="$(curl -sS https://api.anthropic.com/v1/messages/count_tokens \
--config <(printf 'header = "x-api-key: %s"\n' "$API_KEY") \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "$BODY")"
unset API_KEY
if $RAW; then
echo "$RESPONSE"
exit 0
fi
# Surface API errors instead of printing "null".
if echo "$RESPONSE" | jq -e 'has("error")' >/dev/null 2>&1; then
echo "Error from Anthropic API:" >&2
echo "$RESPONSE" | jq -r '.error.type + ": " + .error.message' >&2
exit 1
fi
echo "$RESPONSE" | jq -r '.input_tokens'