mirror of
https://github.com/SeriousBug/dotfiles
synced 2026-06-16 20:35:08 -05:00
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
This commit is contained in:
parent
c82e17654e
commit
b0070920e3
|
|
@ -73,6 +73,7 @@ depends = []
|
||||||
|
|
||||||
[bin.files]
|
[bin.files]
|
||||||
"bin/discord-send" = "~/.local/bin/discord-send"
|
"bin/discord-send" = "~/.local/bin/discord-send"
|
||||||
|
"bin/claude-token-count" = "~/.local/bin/claude-token-count"
|
||||||
|
|
||||||
[bin.variables]
|
[bin.variables]
|
||||||
|
|
||||||
|
|
|
||||||
95
bin/claude-token-count
Executable file
95
bin/claude-token-count
Executable file
|
|
@ -0,0 +1,95 @@
|
||||||
|
#!/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'
|
||||||
64
claude/skills/cc-compact/SKILL.md
Normal file
64
claude/skills/cc-compact/SKILL.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
name: cc-compact
|
||||||
|
description: Summarize ("compact") a past Claude Code session without ingesting its entire history. Resolves the session log, then extracts only the key signals — opening intent, recent prompts, most-edited files, and what the agent was doing last. Use, when asked, to resume long sessions without ingesting entire log.
|
||||||
|
---
|
||||||
|
|
||||||
|
You are reloading the context of a previous Claude Code session so you can
|
||||||
|
**continue that work in this session** — like `/resume` followed by `/compact`, but pre-compacted. The
|
||||||
|
bundled helper script extracts the key signals and prints a compact report.
|
||||||
|
|
||||||
|
**Hard rule: you may NOT read the session `.jsonl` file by any means** — no
|
||||||
|
`cat`, `head`, `tail`, `jq`, `grep`, `Read`, nothing. Run the helper script
|
||||||
|
**once** and work solely from its output. That is the only thing allowed to
|
||||||
|
touch the file.
|
||||||
|
|
||||||
|
The helper script lives at `~/.claude/skills/cc-compact/compact_session.py`.
|
||||||
|
|
||||||
|
## Step 1: Resolve the session
|
||||||
|
|
||||||
|
The argument is one of two forms:
|
||||||
|
|
||||||
|
- **A session id** — when invoked like `/resume claude --resume <id>` or
|
||||||
|
`cc-compact <id>`, the user already gave you the UUID. Pass it as `--id`.
|
||||||
|
- **A session name / title** — free text. Pass it as `--title`; the script
|
||||||
|
matches it against the `ai-title` records inside the logs (case-insensitive
|
||||||
|
substring, newest match wins).
|
||||||
|
|
||||||
|
Run the script exactly once with the matching form:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# By id:
|
||||||
|
python3 ~/.claude/skills/cc-compact/compact_session.py --id <session-uuid>
|
||||||
|
|
||||||
|
# By title:
|
||||||
|
python3 ~/.claude/skills/cc-compact/compact_session.py --title "the session name"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Read the output
|
||||||
|
|
||||||
|
It prints a bounded, XML-tagged report:
|
||||||
|
|
||||||
|
- header: project, git branch, time span, record/prompt counts
|
||||||
|
- the first few exchanges (user prompt + the agent's reply) — the original intent
|
||||||
|
- a few exchanges sampled from the middle (non-overlapping) — the journey
|
||||||
|
- the last few exchanges (user prompt + the agent's reply) — where things were heading
|
||||||
|
- the most-edited files, ranked by edit count
|
||||||
|
- the last several tool calls — what the agent was physically doing last
|
||||||
|
- the final assistant message — what it was saying / waiting on last
|
||||||
|
|
||||||
|
## Step 3: Pick up the work
|
||||||
|
|
||||||
|
The output is context for *you* — treat it like a resumed session, not something
|
||||||
|
to report on. From it, reconstruct what the work was, which files are in play,
|
||||||
|
and what the agent was in the middle of.
|
||||||
|
|
||||||
|
Compaction is lossy — the output may not make the next step unambiguous. Before
|
||||||
|
diving in, decide whether you actually know how to proceed:
|
||||||
|
|
||||||
|
- **If the next step is very clear, last few messages from user confirms what to do**, open with one line confirming what you're
|
||||||
|
resuming, then continue from where it left off.
|
||||||
|
- **If it's ambiguous** (unclear what the user wants next, multiple plausible
|
||||||
|
directions, or the session ended mid-decision), **ask the user how to proceed**
|
||||||
|
before acting. Surface the few plausible next steps you inferred and let them
|
||||||
|
pick or correct you. If there is any doubt, opt to ask the user first.
|
||||||
|
|
||||||
262
claude/skills/cc-compact/compact_session.py
Executable file
262
claude/skills/cc-compact/compact_session.py
Executable file
|
|
@ -0,0 +1,262 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compact a Claude Code session log into a small, bounded summary.
|
||||||
|
|
||||||
|
This reads a session JSONL file *carefully* — it never dumps the whole
|
||||||
|
history. It extracts only the few signals needed to understand what the
|
||||||
|
session was about and what the agent was doing at the end:
|
||||||
|
|
||||||
|
- header metadata (project, branch, time span, message counts)
|
||||||
|
- the first few exchanges (user prompt + the agent's reply, truncated)
|
||||||
|
- a few exchanges randomly sampled from the middle (non-overlapping)
|
||||||
|
- the last few exchanges (user prompt + the agent's reply, truncated)
|
||||||
|
- the most-edited files (top N by edit count)
|
||||||
|
- the final assistant text (what it was saying last)
|
||||||
|
- the last few tool calls (what it was doing last)
|
||||||
|
|
||||||
|
Session resolution (pick one):
|
||||||
|
--file PATH use this JSONL file directly
|
||||||
|
--id UUID find <UUID>.jsonl under the projects dir
|
||||||
|
--title TEXT find the session whose ai-title contains TEXT
|
||||||
|
(case-insensitive substring; newest match wins)
|
||||||
|
|
||||||
|
Sessions live at: ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
|
||||||
|
PROJECTS_DIR = os.path.expanduser("~/.claude/projects")
|
||||||
|
|
||||||
|
# Tools that change files on disk, and which input field holds the path.
|
||||||
|
EDIT_TOOLS = {
|
||||||
|
"Edit": "file_path",
|
||||||
|
"Write": "file_path",
|
||||||
|
"MultiEdit": "file_path",
|
||||||
|
"NotebookEdit": "notebook_path",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def truncate(text, n):
|
||||||
|
"""Collapse whitespace to a single line, then clip to n chars.
|
||||||
|
Use for compact one-liners like tool arguments."""
|
||||||
|
text = " ".join(text.split())
|
||||||
|
return text if len(text) <= n else text[: n - 1] + "…"
|
||||||
|
|
||||||
|
|
||||||
|
def clip(text, n):
|
||||||
|
"""Clip to n chars while preserving newlines/indentation.
|
||||||
|
Use for quoted messages where structure matters."""
|
||||||
|
text = text.strip("\n")
|
||||||
|
return text if len(text) <= n else text[:n].rstrip() + " …[truncated]"
|
||||||
|
|
||||||
|
|
||||||
|
def esc(text):
|
||||||
|
"""Escape XML metacharacters for tag values and attributes."""
|
||||||
|
return str(text).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
|
||||||
|
|
||||||
|
def content_to_text(content):
|
||||||
|
"""A message's .content is either a string or a list of typed blocks."""
|
||||||
|
if isinstance(content, str):
|
||||||
|
return content
|
||||||
|
if isinstance(content, list):
|
||||||
|
parts = [b.get("text", "") for b in content if isinstance(b, dict) and b.get("type") == "text"]
|
||||||
|
return "\n".join(p for p in parts if p)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def is_genuine_prompt(rec):
|
||||||
|
"""A real human-typed prompt: a user message that isn't a tool result,
|
||||||
|
meta record, slash-command wrapper, or interrupt marker."""
|
||||||
|
if rec.get("type") != "user" or rec.get("isMeta"):
|
||||||
|
return False
|
||||||
|
content = rec.get("message", {}).get("content")
|
||||||
|
if not isinstance(content, str):
|
||||||
|
return False
|
||||||
|
s = content.lstrip()
|
||||||
|
if not s:
|
||||||
|
return False
|
||||||
|
skip_prefixes = ("<", "[Request interrupted", "Caveat:")
|
||||||
|
return not s.startswith(skip_prefixes)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_file(args):
|
||||||
|
if args.file:
|
||||||
|
return os.path.expanduser(args.file)
|
||||||
|
|
||||||
|
if args.id:
|
||||||
|
matches = glob.glob(os.path.join(PROJECTS_DIR, "**", f"{args.id}.jsonl"), recursive=True)
|
||||||
|
if not matches:
|
||||||
|
sys.exit(f"No session file found for id {args.id} under {PROJECTS_DIR}")
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
if args.title:
|
||||||
|
needle = args.title.lower()
|
||||||
|
candidates = [] # (mtime, path, title)
|
||||||
|
for path in glob.glob(os.path.join(PROJECTS_DIR, "**", "*.jsonl"), recursive=True):
|
||||||
|
title = None
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
for line in fh:
|
||||||
|
if '"ai-title"' not in line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
rec = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
if rec.get("type") == "ai-title":
|
||||||
|
title = rec.get("aiTitle", "")
|
||||||
|
if needle in title.lower():
|
||||||
|
break
|
||||||
|
title = None
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if title is not None:
|
||||||
|
candidates.append((os.path.getmtime(path), path, title))
|
||||||
|
if not candidates:
|
||||||
|
sys.exit(f"No session whose ai-title contains {args.title!r}")
|
||||||
|
candidates.sort(reverse=True)
|
||||||
|
if len(candidates) > 1:
|
||||||
|
sys.stderr.write("Multiple matches (using newest):\n")
|
||||||
|
for _, path, title in candidates:
|
||||||
|
sys.stderr.write(f" {path} — {title}\n")
|
||||||
|
return candidates[0][1]
|
||||||
|
|
||||||
|
sys.exit("Provide one of --file, --id, or --title")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
g = ap.add_mutually_exclusive_group(required=True)
|
||||||
|
g.add_argument("--file", help="path to a session .jsonl")
|
||||||
|
g.add_argument("--id", help="session UUID")
|
||||||
|
g.add_argument("--title", help="substring of the session's ai-title")
|
||||||
|
ap.add_argument("--first", type=int, default=5, help="how many opening exchanges")
|
||||||
|
ap.add_argument("--last", type=int, default=10, help="how many closing exchanges")
|
||||||
|
ap.add_argument("--middle", type=int, default=5, help="how many exchanges randomly sampled from the middle")
|
||||||
|
ap.add_argument("--top-files", type=int, default=10, help="how many most-edited files")
|
||||||
|
ap.add_argument("--maxlen", type=int, default=800, help="max chars per quoted message")
|
||||||
|
ap.add_argument("--seed", type=int, default=1, help="seed for the middle random sample")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
path = resolve_file(args)
|
||||||
|
|
||||||
|
turns = [] # conversational turns: {"prompt": str, "reply": [text,...]}
|
||||||
|
edit_counts = {} # file path -> edit count
|
||||||
|
last_assistant_text = [] # final text blocks
|
||||||
|
recent_tools = [] # (name, short arg)
|
||||||
|
counts = {} # record type -> count
|
||||||
|
first_ts = last_ts = None
|
||||||
|
cwd = branch = None
|
||||||
|
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
for line in fh:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
rec = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rtype = rec.get("type")
|
||||||
|
counts[rtype] = counts.get(rtype, 0) + 1
|
||||||
|
|
||||||
|
ts = rec.get("timestamp")
|
||||||
|
if ts:
|
||||||
|
first_ts = first_ts or ts
|
||||||
|
last_ts = ts
|
||||||
|
cwd = cwd or rec.get("cwd")
|
||||||
|
branch = branch or rec.get("gitBranch")
|
||||||
|
|
||||||
|
if is_genuine_prompt(rec):
|
||||||
|
turns.append({"prompt": rec["message"]["content"], "reply": []})
|
||||||
|
|
||||||
|
if rtype == "assistant":
|
||||||
|
blocks = rec.get("message", {}).get("content", [])
|
||||||
|
if isinstance(blocks, list):
|
||||||
|
text_here = []
|
||||||
|
for b in blocks:
|
||||||
|
if not isinstance(b, dict):
|
||||||
|
continue
|
||||||
|
if b.get("type") == "text" and b.get("text", "").strip():
|
||||||
|
text_here.append(b["text"])
|
||||||
|
elif b.get("type") == "tool_use":
|
||||||
|
name = b.get("name", "?")
|
||||||
|
inp = b.get("input", {}) or {}
|
||||||
|
if name in EDIT_TOOLS:
|
||||||
|
fp = inp.get(EDIT_TOOLS[name])
|
||||||
|
if fp:
|
||||||
|
edit_counts[fp] = edit_counts.get(fp, 0) + 1
|
||||||
|
arg = inp.get("file_path") or inp.get("command") or inp.get("description") or inp.get("path") or ""
|
||||||
|
recent_tools.append((name, truncate(str(arg), 300)))
|
||||||
|
if text_here:
|
||||||
|
last_assistant_text = text_here # keep only the latest turn's text
|
||||||
|
if turns: # attach the agent's reply to the current turn
|
||||||
|
turns[-1]["reply"].extend(text_here)
|
||||||
|
|
||||||
|
# Non-overlapping index sets: first wins, then last, then the middle is
|
||||||
|
# sampled only from what's left between them. If the regions collide
|
||||||
|
# (short session), the overlap is simply dropped.
|
||||||
|
n = len(turns)
|
||||||
|
first_idx = list(range(min(args.first, n)))
|
||||||
|
last_start = max(len(first_idx), n - args.last)
|
||||||
|
last_idx = list(range(last_start, n))
|
||||||
|
mid_pool = list(range(len(first_idx), last_start))
|
||||||
|
if args.seed is not None:
|
||||||
|
random.seed(args.seed)
|
||||||
|
mid_idx = sorted(random.sample(mid_pool, min(args.middle, len(mid_pool))))
|
||||||
|
|
||||||
|
# ---- emit a bounded, XML-tagged report (newlines preserved) ----
|
||||||
|
out = sys.stdout.write
|
||||||
|
|
||||||
|
out("<session-summary>\n")
|
||||||
|
out(f" <file>{esc(path)}</file>\n")
|
||||||
|
out(f" <project>{esc(cwd)}</project>\n")
|
||||||
|
out(f" <git-branch>{esc(branch)}</git-branch>\n")
|
||||||
|
out(f' <time-span start="{esc(first_ts)}" end="{esc(last_ts)}" />\n')
|
||||||
|
out(f" <records>{esc(', '.join(f'{k}={v}' for k, v in sorted(counts.items())))}</records>\n")
|
||||||
|
out(f" <user-prompts count=\"{len(turns)}\" />\n")
|
||||||
|
out("</session-summary>\n")
|
||||||
|
|
||||||
|
def emit_exchanges(label, indices):
|
||||||
|
if not indices:
|
||||||
|
return
|
||||||
|
out(f'\n<exchanges section="{label}">\n')
|
||||||
|
for i in indices:
|
||||||
|
t = turns[i]
|
||||||
|
reply = "\n".join(t["reply"]).strip()
|
||||||
|
out(f' <exchange n="{i + 1}">\n')
|
||||||
|
out(f" <user>\n{clip(t['prompt'], args.maxlen)}\n </user>\n")
|
||||||
|
if reply:
|
||||||
|
out(f" <agent>\n{clip(reply, args.maxlen)}\n </agent>\n")
|
||||||
|
else:
|
||||||
|
out(" <agent note=\"no text reply — tool calls only\" />\n")
|
||||||
|
out(" </exchange>\n")
|
||||||
|
out("</exchanges>\n")
|
||||||
|
|
||||||
|
emit_exchanges("first", first_idx)
|
||||||
|
emit_exchanges("sampled-middle", mid_idx)
|
||||||
|
emit_exchanges("last", last_idx)
|
||||||
|
|
||||||
|
out(f'\n<most-edited-files top="{args.top_files}">\n')
|
||||||
|
for fp, c in sorted(edit_counts.items(), key=lambda kv: -kv[1])[: args.top_files]:
|
||||||
|
out(f' <file edits="{c}">{esc(fp)}</file>\n')
|
||||||
|
out("</most-edited-files>\n")
|
||||||
|
|
||||||
|
out("\n<last-tool-calls>\n")
|
||||||
|
for name, arg in recent_tools[-8:]:
|
||||||
|
out(f' <call tool="{esc(name)}">{esc(arg)}</call>\n')
|
||||||
|
out("</last-tool-calls>\n")
|
||||||
|
|
||||||
|
out("\n<final-agent-message>\n")
|
||||||
|
out(clip("\n".join(last_assistant_text), 2000) + "\n")
|
||||||
|
out("</final-agent-message>\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
28
claude/skills/new-work/SKILL.md
Normal file
28
claude/skills/new-work/SKILL.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
name: new-work
|
||||||
|
description: Prepare the repo to start a new piece of work. Use this skill when the user asks to start a new task, begin new work, create a new branch, or start working on something new.
|
||||||
|
---
|
||||||
|
|
||||||
|
You are helping the user start a new piece of work in a git repository. Follow these steps in order:
|
||||||
|
|
||||||
|
## Step 1: Check for uncommitted changes
|
||||||
|
|
||||||
|
Run `git status --short` to check for uncommitted changes.
|
||||||
|
|
||||||
|
If there are uncommitted changes:
|
||||||
|
- Evaluate the changes:
|
||||||
|
- If there are build artifacts, etc. then ignore them. If they are tracked by git, note for user that they might want to gitingore these.
|
||||||
|
- If changes look important, such as code changes, new code files that are not empty, changes to build workflows etc., then ask user if they want to stash
|
||||||
|
|
||||||
|
## Step 2: Switch to main and pull latest
|
||||||
|
|
||||||
|
- Attempt to check out `main`.
|
||||||
|
- If that fails because the branch does not exist, check out `master` instead.
|
||||||
|
- Then pull the latest changes:
|
||||||
|
|
||||||
|
## Step 3: Create a new branch
|
||||||
|
|
||||||
|
Based on the description of the work the user has provided, create a short, descriptive, kebab-case branch name (e.g. `add-user-auth`, `fix-checkout-crash`, `refactor-api-client`).
|
||||||
|
Create and check out the new branch.
|
||||||
|
Once branch is created, proceed with work as usual.
|
||||||
|
|
||||||
1
setup.sh
1
setup.sh
|
|
@ -259,6 +259,7 @@ echo ""
|
||||||
echo "Setting up directory symlinks..."
|
echo "Setting up directory symlinks..."
|
||||||
DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
ensure_dir_symlink "$DOTFILES_DIR/fish/functions" "~/.config/fish/functions" "$FORCE_DEPLOY"
|
ensure_dir_symlink "$DOTFILES_DIR/fish/functions" "~/.config/fish/functions" "$FORCE_DEPLOY"
|
||||||
|
ensure_dir_symlink "$DOTFILES_DIR/claude/skills" "~/.claude/skills" "$FORCE_DEPLOY"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✓ Setup complete!"
|
echo "✓ Setup complete!"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue