-
-
Notifications
You must be signed in to change notification settings - Fork 24.8k
feat(skill): ck — persistent per-project memory for Claude Code #959
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| --- | ||
| name: ck | ||
| description: Persistent per-project memory for Claude Code. Auto-loads project context on session start, tracks sessions with git activity, and writes to native memory. Commands run deterministic Node.js scripts — behavior is consistent across model versions. | ||
| origin: community | ||
| version: 2.0.0 | ||
| author: sreedhargs89 | ||
| repo: https://github.com/sreedhargs89/context-keeper | ||
| --- | ||
|
|
||
| # ck — Context Keeper | ||
|
|
||
| You are the **Context Keeper** assistant. When the user invokes any `/ck:*` command, | ||
| run the corresponding Node.js script and present its stdout to the user verbatim. | ||
| Scripts live at: `~/.claude/skills/ck/commands/` (expand `~` with `$HOME`). | ||
|
|
||
| --- | ||
|
|
||
| ## Data Layout | ||
|
|
||
| ``` | ||
| ~/.claude/ck/ | ||
| ├── projects.json ← path → {name, contextDir, lastUpdated} | ||
| └── contexts/<name>/ | ||
| ├── context.json ← SOURCE OF TRUTH (structured JSON, v2) | ||
| └── CONTEXT.md ← generated view — do not hand-edit | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Commands | ||
|
|
||
| ### `/ck:init` — Register a Project | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/init.mjs" | ||
| ``` | ||
| The script outputs JSON with auto-detected info. Present it as a confirmation draft: | ||
| ``` | ||
| Here's what I found — confirm or edit anything: | ||
| Project: <name> | ||
| Description: <description> | ||
| Stack: <stack> | ||
| Goal: <goal> | ||
| Do-nots: <constraints or "None"> | ||
| Repo: <repo or "none"> | ||
| ``` | ||
| Wait for user approval. Apply any edits. Then pipe confirmed JSON to save.mjs --init: | ||
| ```bash | ||
| echo '<confirmed-json>' | node "$HOME/.claude/skills/ck/commands/save.mjs" --init | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Using single-quoted Prompt for AI agents |
||
| ``` | ||
| Confirmed JSON schema: `{"name":"...","path":"...","description":"...","stack":["..."],"goal":"...","constraints":["..."],"repo":"..." }` | ||
|
|
||
| --- | ||
|
|
||
| ### `/ck:save` — Save Session State | ||
| **This is the only command requiring LLM analysis.** Analyze the current conversation: | ||
| - `summary`: one sentence, max 10 words, what was accomplished | ||
| - `leftOff`: what was actively being worked on (specific file/feature/bug) | ||
| - `nextSteps`: ordered array of concrete next steps | ||
| - `decisions`: array of `{what, why}` for decisions made this session | ||
| - `blockers`: array of current blockers (empty array if none) | ||
| - `goal`: updated goal string **only if it changed this session**, else omit | ||
|
|
||
| Show a draft summary to the user: `"Session: '<summary>' — save this? (yes / edit)"` | ||
| Wait for confirmation. Then pipe to save.mjs: | ||
| ```bash | ||
| echo '<json>' | node "$HOME/.claude/skills/ck/commands/save.mjs" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Using single-quoted Prompt for AI agents |
||
| ``` | ||
| JSON schema (exact): `{"summary":"...","leftOff":"...","nextSteps":["..."],"decisions":[{"what":"...","why":"..."}],"blockers":["..."]}` | ||
| Display the script's stdout confirmation verbatim. | ||
|
|
||
| --- | ||
|
|
||
| ### `/ck:resume [name|number]` — Full Briefing | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/resume.mjs" [arg] | ||
| ``` | ||
| Display output verbatim. Then ask: "Continue from here? Or has anything changed?" | ||
| If user reports changes → run `/ck:save` immediately. | ||
|
|
||
| --- | ||
|
|
||
| ### `/ck:info [name|number]` — Quick Snapshot | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/info.mjs" [arg] | ||
| ``` | ||
| Display output verbatim. No follow-up question. | ||
|
|
||
| --- | ||
|
|
||
| ### `/ck:list` — Portfolio View | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/list.mjs" | ||
| ``` | ||
| Display output verbatim. If user replies with a number or name → run `/ck:resume`. | ||
|
|
||
| --- | ||
|
|
||
| ### `/ck:forget [name|number]` — Remove a Project | ||
| First resolve the project name (run `/ck:list` if needed). | ||
| Ask: `"This will permanently delete context for '<name>'. Are you sure? (yes/no)"` | ||
| If yes: | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/forget.mjs" [name] | ||
| ``` | ||
| Display confirmation verbatim. | ||
|
|
||
| --- | ||
|
|
||
| ### `/ck:migrate` — Convert v1 Data to v2 | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/migrate.mjs" | ||
| ``` | ||
| For a dry run first: | ||
| ```bash | ||
| node "$HOME/.claude/skills/ck/commands/migrate.mjs" --dry-run | ||
| ``` | ||
| Display output verbatim. Migrates all v1 CONTEXT.md + meta.json files to v2 context.json. | ||
| Originals are backed up as `meta.json.v1-backup` — nothing is deleted. | ||
|
|
||
| --- | ||
|
|
||
| ## SessionStart Hook | ||
|
|
||
| The hook at `~/.claude/skills/ck/hooks/session-start.mjs` must be registered in | ||
| `~/.claude/settings.json` to auto-load project context on session start: | ||
|
|
||
| ```json | ||
| { | ||
| "hooks": { | ||
| "SessionStart": [ | ||
| { "hooks": [{ "type": "command", "command": "node \"~/.claude/skills/ck/hooks/session-start.mjs\"" }] } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: SessionStart hook example quotes Prompt for AI agents |
||
| ] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The hook injects ~100 tokens per session (compact 5-line summary). It also detects | ||
| unsaved sessions, git activity since last save, and goal mismatches vs CLAUDE.md. | ||
|
|
||
| --- | ||
|
|
||
| ## Rules | ||
| - Always expand `~` as `$HOME` in Bash calls. | ||
| - Commands are case-insensitive: `/CK:SAVE`, `/ck:save`, `/Ck:Save` all work. | ||
| - If a script exits with code 1, display its stdout as an error message. | ||
| - Never edit `context.json` or `CONTEXT.md` directly — always use the scripts. | ||
| - If `projects.json` is malformed, tell the user and offer to reset it to `{}`. | ||
|
Comment on lines
+10
to
+147
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Missing required skill documentation sections. Per coding guidelines, skills under Consider adding these sections to comply with the repository's skill formatting standards: 📝 Suggested structure to add after line 9## When to Use
- When starting a new Claude Code session and you want to quickly resume context from your last session
- When working across multiple projects and need to track session state for each
- When you want decisions, blockers, and next steps to persist between sessions
- When onboarding to a project and need a quick briefing on current state
## How It Works
1. **Registration**: `/ck:init` detects project metadata (stack, goal, constraints) from config files and CLAUDE.md
2. **Saving**: `/ck:save` captures session state (decisions, blockers, next steps) to `context.json` and writes to native memory
3. **Resuming**: `/ck:resume` loads the full briefing box; the session-start hook auto-injects a compact summary (~100 tokens)
4. **Portfolio**: `/ck:list` shows all registered projects with staleness indicators
## Examples
### Initialize a new project/ck:init /ck:save /ck:resume my-app /ck:resume 2 Verify each finding against the current code and only fix it if needed. In |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * ck — Context Keeper v2 | ||
| * forget.mjs — remove a project's context and registry entry | ||
| * | ||
| * Usage: node forget.mjs [name|number] | ||
| * stdout: confirmation or error | ||
| * exit 0: success exit 1: not found | ||
| * | ||
| * Note: SKILL.md instructs Claude to ask "Are you sure?" before calling this script. | ||
| * This script is the "do it" step — no confirmation prompt here. | ||
| */ | ||
|
|
||
| import { rmSync } from 'fs'; | ||
| import { resolve } from 'path'; | ||
| import { resolveContext, readProjects, writeProjects, CONTEXTS_DIR } from './shared.mjs'; | ||
|
|
||
| const arg = process.argv[2]; | ||
| const cwd = process.env.PWD || process.cwd(); | ||
|
|
||
| const resolved = resolveContext(arg, cwd); | ||
| if (!resolved) { | ||
| const hint = arg ? `No project matching "${arg}".` : 'This directory is not registered.'; | ||
| console.log(`${hint}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const { name, contextDir, projectPath } = resolved; | ||
|
|
||
| // Remove context directory | ||
| const contextDirPath = resolve(CONTEXTS_DIR, contextDir); | ||
| try { | ||
| rmSync(contextDirPath, { recursive: true, force: true }); | ||
| } catch (e) { | ||
| console.log(`ck: could not remove context directory — ${e.message}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| // Remove from projects.json | ||
| const projects = readProjects(); | ||
| delete projects[projectPath]; | ||
| writeProjects(projects); | ||
|
|
||
| console.log(`✓ Context for '${name}' removed.`); | ||
|
Comment on lines
+39
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential state inconsistency if If the context directory is deleted but 🛡️ Proposed fix // Remove from projects.json
-const projects = readProjects();
-delete projects[projectPath];
-writeProjects(projects);
+try {
+ const projects = readProjects();
+ delete projects[projectPath];
+ writeProjects(projects);
+} catch (e) {
+ console.log(`⚠ Context directory removed but projects.json update failed: ${e.message}`);
+ process.exit(1);
+}
console.log(`✓ Context for '${name}' removed.`);🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * ck — Context Keeper v2 | ||
| * info.mjs — quick read-only context snapshot | ||
| * | ||
| * Usage: node info.mjs [name|number] | ||
| * stdout: compact info block | ||
| * exit 0: success exit 1: not found | ||
| */ | ||
|
|
||
| import { resolveContext, renderInfoBlock } from './shared.mjs'; | ||
|
|
||
| const arg = process.argv[2]; | ||
| const cwd = process.env.PWD || process.cwd(); | ||
|
|
||
| const resolved = resolveContext(arg, cwd); | ||
| if (!resolved) { | ||
| const hint = arg ? `No project matching "${arg}".` : 'This directory is not registered.'; | ||
| console.log(`${hint} Run /ck:init to register it.`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log(''); | ||
| console.log(renderInfoBlock(resolved.context)); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * ck — Context Keeper v2 | ||
| * init.mjs — auto-detect project info and output JSON for Claude to confirm | ||
| * | ||
| * Usage: node init.mjs | ||
| * stdout: JSON with auto-detected project info | ||
| * exit 0: success exit 1: error | ||
| */ | ||
|
|
||
| import { readFileSync, existsSync } from 'fs'; | ||
| import { resolve, basename } from 'path'; | ||
| import { readProjects } from './shared.mjs'; | ||
|
|
||
| const cwd = process.env.PWD || process.cwd(); | ||
| const projects = readProjects(); | ||
|
|
||
| const output = { | ||
| path: cwd, | ||
| name: null, | ||
| description: null, | ||
| stack: [], | ||
| goal: null, | ||
| constraints: [], | ||
| repo: null, | ||
| alreadyRegistered: !!projects[cwd], | ||
| }; | ||
|
|
||
| function readFile(filename) { | ||
| const p = resolve(cwd, filename); | ||
| if (!existsSync(p)) return null; | ||
| try { return readFileSync(p, 'utf8'); } catch { return null; } | ||
| } | ||
|
|
||
| function extractSection(md, heading) { | ||
| const re = new RegExp(`## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Section extraction regex is newline-fragile; it won’t match CRLF ( Prompt for AI agents |
||
| const m = md.match(re); | ||
| return m ? m[1].trim() : null; | ||
| } | ||
|
|
||
| // ── package.json ────────────────────────────────────────────────────────────── | ||
| const pkg = readFile('package.json'); | ||
| if (pkg) { | ||
| try { | ||
| const parsed = JSON.parse(pkg); | ||
| if (parsed.name && !output.name) output.name = parsed.name; | ||
| if (parsed.description && !output.description) output.description = parsed.description; | ||
|
|
||
| // Detect stack from dependencies | ||
| const deps = Object.keys({ ...(parsed.dependencies || {}), ...(parsed.devDependencies || {}) }); | ||
| const stackMap = { | ||
| next: 'Next.js', react: 'React', vue: 'Vue', svelte: 'Svelte', astro: 'Astro', | ||
| express: 'Express', fastify: 'Fastify', hono: 'Hono', nestjs: 'NestJS', | ||
| typescript: 'TypeScript', prisma: 'Prisma', drizzle: 'Drizzle', | ||
| '@neondatabase/serverless': 'Neon', '@upstash/redis': 'Upstash Redis', | ||
| '@clerk/nextjs': 'Clerk', stripe: 'Stripe', tailwindcss: 'Tailwind CSS', | ||
| }; | ||
| for (const [dep, label] of Object.entries(stackMap)) { | ||
| if (deps.includes(dep) && !output.stack.includes(label)) { | ||
| output.stack.push(label); | ||
| } | ||
| } | ||
| if (deps.includes('typescript') || existsSync(resolve(cwd, 'tsconfig.json'))) { | ||
| if (!output.stack.includes('TypeScript')) output.stack.push('TypeScript'); | ||
| } | ||
| } catch { /* malformed package.json */ } | ||
| } | ||
|
|
||
| // ── go.mod ──────────────────────────────────────────────────────────────────── | ||
| const goMod = readFile('go.mod'); | ||
| if (goMod) { | ||
| if (!output.stack.includes('Go')) output.stack.push('Go'); | ||
| const modName = goMod.match(/^module\s+(\S+)/m)?.[1]; | ||
| if (modName && !output.name) output.name = modName.split('/').pop(); | ||
| } | ||
|
|
||
| // ── Cargo.toml ──────────────────────────────────────────────────────────────── | ||
| const cargo = readFile('Cargo.toml'); | ||
| if (cargo) { | ||
| if (!output.stack.includes('Rust')) output.stack.push('Rust'); | ||
| const crateName = cargo.match(/^name\s*=\s*"(.+?)"/m)?.[1]; | ||
| if (crateName && !output.name) output.name = crateName; | ||
| } | ||
|
|
||
| // ── pyproject.toml ──────────────────────────────────────────────────────────── | ||
| const pyproject = readFile('pyproject.toml'); | ||
| if (pyproject) { | ||
| if (!output.stack.includes('Python')) output.stack.push('Python'); | ||
| const pyName = pyproject.match(/^name\s*=\s*"(.+?)"/m)?.[1]; | ||
| if (pyName && !output.name) output.name = pyName; | ||
| } | ||
|
|
||
| // ── .git/config (repo URL) ──────────────────────────────────────────────────── | ||
| const gitConfig = readFile('.git/config'); | ||
| if (gitConfig) { | ||
| const repoMatch = gitConfig.match(/url\s*=\s*(.+)/); | ||
| if (repoMatch) output.repo = repoMatch[1].trim(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Repo URL from .git/config is emitted verbatim; if the remote contains embedded credentials (https://token@host/...), they will be exposed in output and persisted. Sanitize userinfo before saving. Prompt for AI agents |
||
| } | ||
|
|
||
| // ── CLAUDE.md ───────────────────────────────────────────────────────────────── | ||
| const claudeMd = readFile('CLAUDE.md'); | ||
| if (claudeMd) { | ||
| const goal = extractSection(claudeMd, 'Current Goal'); | ||
| if (goal && !output.goal) output.goal = goal.split('\n')[0].trim(); | ||
|
|
||
| const doNot = extractSection(claudeMd, 'Do Not Do'); | ||
| if (doNot) { | ||
| const bullets = doNot.split('\n') | ||
| .filter(l => /^[-*]\s+/.test(l)) | ||
| .map(l => l.replace(/^[-*]\s+/, '').trim()); | ||
| output.constraints = bullets; | ||
| } | ||
|
|
||
| const stack = extractSection(claudeMd, 'Tech Stack'); | ||
| if (stack && output.stack.length === 0) { | ||
| output.stack = stack.split(/[,\n]/).map(s => s.replace(/^[-*]\s+/, '').trim()).filter(Boolean); | ||
| } | ||
|
|
||
| // Description from first section or "What This Is" | ||
| const whatItIs = extractSection(claudeMd, 'What This Is') || extractSection(claudeMd, 'About'); | ||
| if (whatItIs && !output.description) output.description = whatItIs.split('\n')[0].trim(); | ||
| } | ||
|
|
||
| // ── README.md (description fallback) ───────────────────────────────────────── | ||
| const readme = readFile('README.md'); | ||
| if (readme && !output.description) { | ||
| // First non-header, non-badge, non-empty paragraph | ||
| const lines = readme.split('\n'); | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('!') && !trimmed.startsWith('>') && !trimmed.startsWith('[') && trimmed !== '---' && trimmed !== '___') { | ||
| output.description = trimmed.slice(0, 120); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // ── Name fallback: directory name ───────────────────────────────────────────── | ||
| if (!output.name) { | ||
| output.name = basename(cwd).toLowerCase().replace(/\s+/g, '-'); | ||
| } | ||
|
|
||
| console.log(JSON.stringify(output, null, 2)); | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: User-facing skill metadata links to an external GitHub repo, which violates the supply-chain hardening guidance to avoid unvetted external repositories.
Prompt for AI agents