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
147 changes: 147 additions & 0 deletions skills/ck/SKILL.md
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
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 27, 2026

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
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/SKILL.md, line 7:

<comment>User-facing skill metadata links to an external GitHub repo, which violates the supply-chain hardening guidance to avoid unvetted external repositories.</comment>

<file context>
@@ -0,0 +1,147 @@
+origin: community
+version: 2.0.0
+author: sreedhargs89
+repo: https://github.com/sreedhargs89/context-keeper
+---
+
</file context>
Fix with Cubic

---

# 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
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Using single-quoted echo '<confirmed-json>' is unsafe for JSON that may contain apostrophes; it can break the shell string or allow unintended shell interpretation. Use a quoted heredoc or printf-safe piping instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/SKILL.md, line 48:

<comment>Using single-quoted `echo '<confirmed-json>'` is unsafe for JSON that may contain apostrophes; it can break the shell string or allow unintended shell interpretation. Use a quoted heredoc or printf-safe piping instead.</comment>

<file context>
@@ -0,0 +1,147 @@
+```
+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
+```
+Confirmed JSON schema: `{"name":"...","path":"...","description":"...","stack":["..."],"goal":"...","constraints":["..."],"repo":"..." }`
</file context>
Fix with Cubic

```
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"
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Using single-quoted echo '<json>' is unsafe for JSON that may contain apostrophes; it can break the shell string or allow unintended shell interpretation. Use a quoted heredoc or printf-safe piping instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/SKILL.md, line 66:

<comment>Using single-quoted `echo '<json>'` is unsafe for JSON that may contain apostrophes; it can break the shell string or allow unintended shell interpretation. Use a quoted heredoc or printf-safe piping instead.</comment>

<file context>
@@ -0,0 +1,147 @@
+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"
+```
+JSON schema (exact): `{"summary":"...","leftOff":"...","nextSteps":["..."],"decisions":[{"what":"...","why":"..."}],"blockers":["..."]}`
</file context>
Fix with Cubic

```
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\"" }] }
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: SessionStart hook example quotes ~, preventing tilde expansion and likely breaking the hook command.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/SKILL.md, line 131:

<comment>SessionStart hook example quotes `~`, preventing tilde expansion and likely breaking the hook command.</comment>

<file context>
@@ -0,0 +1,147 @@
+{
+  "hooks": {
+    "SessionStart": [
+      { "hooks": [{ "type": "command", "command": "node \"~/.claude/skills/ck/hooks/session-start.mjs\"" }] }
+    ]
+  }
</file context>
Fix with Cubic

]
}
}
```

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 skills/**/*.md must include 'When to Use', 'How It Works', and 'Examples' sections. This SKILL.md is missing all three.

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

Claude auto-detects project info and asks for confirmation before saving.

### Save session state

/ck:save

Claude analyzes the conversation and proposes a summary to save.

### Resume a project by name

/ck:resume my-app

/ck:resume 2


As per coding guidelines: "Skills must be formatted as Markdown files with clear sections including 'When to Use', 'How It Works', and 'Examples'".
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @skills/ck/SKILL.md around lines 10 - 147, The SKILL.md is missing the
required documentation sections; update SKILL.md by adding three markdown
sections: "When to Use" (bullet scenarios for using /ck:init, /ck:save,
/ck:resume, /ck:list, /ck:forget, /ck:migrate and the session-start hook), "How
It Works" (brief numbered steps describing Registration (/ck:init → save.mjs),
Saving (/ck:save analysis → save.mjs), Resuming (/ck:resume and session-start
hook injection), and Migration (/ck:migrate)), and "Examples" (short sample
invocations and expected outputs for /ck:init, /ck:save, /ck:resume), ensuring
wording matches the repository skill formatting guidelines and that the examples
reference the actual command names used in the file.


</details>

<!-- fingerprinting:phantom:medusa:ocelot:57dcdb90-4846-489b-bd12-c20feb894a8a -->

<!-- This is an auto-generated comment by CodeRabbit -->

44 changes: 44 additions & 0 deletions skills/ck/commands/forget.mjs
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential state inconsistency if writeProjects fails.

If the context directory is deleted but writeProjects throws (e.g., disk full, permissions), projects.json will still reference the deleted context. Consider wrapping in try/catch to handle this edge case gracefully.

🛡️ 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
Verify each finding against the current code and only fix it if needed.

In `@skills/ck/commands/forget.mjs` around lines 39 - 44, The code deletes the
context directory and then updates projects.json via
readProjects()/writeProjects(), which can leave projects.json pointing to a
removed context if writeProjects fails; wrap the remove-and-write sequence in a
try/catch around the operations that touch projects (use readProjects, delete
projects[projectPath], writeProjects) and on error restore the deleted entry
(re-add projects[projectPath] = originalValue) or recreate the directory as
appropriate, then log a clear error including the project name variable (name)
and the error so the caller can retry or clean up; ensure the catch does not
swallow errors and exits with a non-zero status if unrecoverable.

24 changes: 24 additions & 0 deletions skills/ck/commands/info.mjs
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));
143 changes: 143 additions & 0 deletions skills/ck/commands/init.mjs
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## |$)`);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Section extraction regex is newline-fragile; it won’t match CRLF (\r\n) headings, causing CLAUDE.md sections to be skipped on Windows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/commands/init.mjs, line 36:

<comment>Section extraction regex is newline-fragile; it won’t match CRLF (`\r\n`) headings, causing CLAUDE.md sections to be skipped on Windows.</comment>

<file context>
@@ -0,0 +1,143 @@
+}
+
+function extractSection(md, heading) {
+  const re = new RegExp(`## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`);
+  const m = md.match(re);
+  return m ? m[1].trim() : null;
</file context>
Fix with Cubic

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();
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 27, 2026

Choose a reason for hiding this comment

The 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
Check if this issue is valid — if so, understand the root cause and fix it. At skills/ck/commands/init.mjs, line 97:

<comment>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.</comment>

<file context>
@@ -0,0 +1,143 @@
+const gitConfig = readFile('.git/config');
+if (gitConfig) {
+  const repoMatch = gitConfig.match(/url\s*=\s*(.+)/);
+  if (repoMatch) output.repo = repoMatch[1].trim();
+}
+
</file context>
Fix with Cubic

}

// ── 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));
Loading