claude-teach 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # claude-teach
2
+
3
+ **Ask any question about any codebase and get answers with exact file and line citations — powered by Claude.**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/claude-teach)](https://www.npmjs.com/package/claude-teach)
6
+ [![npm downloads](https://img.shields.io/npm/dm/claude-teach)](https://www.npmjs.com/package/claude-teach)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
9
+
10
+ `claude-teach` loads an entire codebase into memory and opens an interactive terminal session where you can ask questions about it. Claude answers using the actual code — not general knowledge — and cites exact file paths and line numbers. When you're done, export the whole session as a structured course document.
11
+
12
+ ![Demo](demo.gif)
13
+
14
+ ---
15
+
16
+ ## Why this exists
17
+
18
+ Understanding an unfamiliar codebase is one of the most time-consuming parts of software engineering. `claude-teach` gives you a guide who has read every file and can answer:
19
+
20
+ - *"How does authentication work in this app?"*
21
+ - *"Walk me through the request lifecycle end to end"*
22
+ - *"What's the correct way to add a new API endpoint here?"*
23
+ - *"Where is rate limiting implemented?"*
24
+
25
+ Every answer cites the real `file:line`. No hallucination. No guessing.
26
+
27
+ ---
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npx claude-teach <target> # run without installing
33
+ npm install -g claude-teach # or install globally
34
+ ```
35
+
36
+ Requires Node.js 18+ and `ANTHROPIC_API_KEY`:
37
+
38
+ ```bash
39
+ export ANTHROPIC_API_KEY=sk-ant-...
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Usage
45
+
46
+ ```bash
47
+ npx claude-teach https://github.com/expressjs/express # GitHub repo
48
+ npx claude-teach expressjs/express # short form
49
+ npx claude-teach . # current directory
50
+ npx claude-teach ~/projects/my-app # local path
51
+ npx claude-teach . --export onboarding-guide.md # export session as doc
52
+ npx claude-teach . --no-stream # disable streaming
53
+ ```
54
+
55
+ ### Session example
56
+
57
+ ```
58
+ Loading repository: expressjs/express
59
+ Read 47 files from expressjs/express
60
+
61
+ Ask questions about this codebase. Type "exit" or Ctrl+C to quit.
62
+
63
+ You: how does routing work?
64
+
65
+ Claude: Express routing is built around a Router class in `lib/router/index.js`.
66
+
67
+ When you call `app.get('/path', handler)`, Express calls `router.route(path)`
68
+ (lib/router/index.js:491) which creates a Route object (lib/router/route.js)
69
+ and adds it to the stack.
70
+
71
+ Each request passes through `router.handle()` (lib/router/index.js:136), which
72
+ iterates the Layer stack. Each Layer wraps a middleware function or a Route...
73
+
74
+ You: what's a Layer?
75
+
76
+ Claude: A Layer is defined in `lib/router/layer.js:1`. It wraps:
77
+ - A path pattern (converted to regexp via path-to-regexp)
78
+ - A handler function
79
+ - Match options (case sensitivity, strict mode)
80
+
81
+ The key method is `layer.match(path)` at line 103...
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Options
87
+
88
+ | Flag | Default | Description |
89
+ |---|---|---|
90
+ | `--export <file>` | — | Export full session as a course document on exit |
91
+ | `--no-stream` | streaming on | Disable token-by-token streaming |
92
+ | `--github-token <token>` | `$GITHUB_TOKEN` | Token for private repos / higher rate limits |
93
+
94
+ ### Environment variables
95
+
96
+ | Variable | Required | Description |
97
+ |---|---|---|
98
+ | `ANTHROPIC_API_KEY` | Yes | Your Anthropic API key |
99
+ | `GITHUB_TOKEN` | No | Raises GitHub rate limit from 60 to 5,000 req/hr |
100
+
101
+ ---
102
+
103
+ ## How it works
104
+
105
+ ```
106
+ Input (URL or path)
107
+ → Load repo: fetch files via GitHub API or walk filesystem
108
+ → Build system prompt: embed all file contents + file index
109
+ → REPL loop: each question appended to message history, streamed response
110
+ → On exit with --export: reorganize Q&A into structured course document
111
+ ```
112
+
113
+ **File prioritization:** When the repo is too large for one context window, files are prioritized by: entry points and configs first, then by directory depth (shallower = higher priority). Up to ~80,000 chars total.
114
+
115
+ **Citations:** Claude is instructed to always reference code as `filename.ts:42`. It traces through actual functions in the loaded files rather than relying on general framework knowledge.
116
+
117
+ **Context window management:** Session history is trimmed to the last 20 turns to prevent context overflow during long sessions.
118
+
119
+ ---
120
+
121
+ ## The `--export` course document
122
+
123
+ When you quit with `--export course.md`, Claude generates a proper onboarding document from your session:
124
+
125
+ ```markdown
126
+ # expressjs/express — Codebase Guide
127
+
128
+ ## Introduction
129
+ ## Table of Contents
130
+ 1. Routing System
131
+ 2. Middleware Pipeline
132
+ 3. Request / Response Lifecycle
133
+
134
+ ## 1. Routing System
135
+ Routing centers on the Router class (`lib/router/index.js`). When you call
136
+ `app.get('/path', fn)`, Express calls `router.route(path)` at line 491...
137
+
138
+ ## Summary
139
+ After reading this guide you understand how requests flow through Express...
140
+ ```
141
+
142
+ Use cases: team onboarding docs, open source contributor guides, personal reference for unfamiliar codebases.
143
+
144
+ ---
145
+
146
+ ## Questions that work well
147
+
148
+ ```
149
+ how does [authentication / caching / queuing] work?
150
+ walk me through a request lifecycle end to end
151
+ what's the difference between [classA] and [classB]?
152
+ how do I add a new [endpoint / command / model]?
153
+ what does [FileName.ts] do and why does it exist?
154
+ which files would I need to change to add [feature]?
155
+ what are the most important files to understand first?
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Cost
161
+
162
+ Typical 10–15 question session on a medium repo: **~$0.10–0.30**. Export adds ~$0.05–0.10.
163
+
164
+ ---
165
+
166
+ ## Troubleshooting
167
+
168
+ **"I don't have information about that file"** — Repo too large to fully load. Ask about core/entry-point files which are always prioritized.
169
+
170
+ **GitHub rate limit errors** — `npx claude-teach owner/repo --github-token $(gh auth token)`
171
+
172
+ ---
173
+
174
+ ## Contributing
175
+
176
+ ```bash
177
+ git clone https://github.com/amritessh/claude-teach && cd claude-teach
178
+ npm install && npm run dev
179
+ node dist/index.js .
180
+ ```
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,4 @@
1
+ import type { RepoContext } from "./lib";
2
+ import type { Turn } from "./session";
3
+ export declare function exportCourse(ctx: RepoContext, turns: Turn[], outputPath: string): Promise<void>;
4
+ //# sourceMappingURL=course.d.ts.map
package/dist/course.js ADDED
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.exportCourse = exportCourse;
37
+ const lib_1 = require("./lib");
38
+ const fs = __importStar(require("fs"));
39
+ async function exportCourse(ctx, turns, outputPath) {
40
+ if (turns.length === 0) {
41
+ console.log("No session turns to export.");
42
+ return;
43
+ }
44
+ const sessionTranscript = turns
45
+ .map((t, i) => `## Q${i + 1}: ${t.question}\n\n${t.answer}`)
46
+ .join("\n\n---\n\n");
47
+ const prompt = `You are creating a structured course document from a Q&A session about the codebase "${ctx.owner}/${ctx.repo}".
48
+
49
+ Here is the Q&A session transcript:
50
+
51
+ ${sessionTranscript}
52
+
53
+ Convert this into a well-structured course document with:
54
+
55
+ # ${ctx.owner}/${ctx.repo} — Codebase Guide
56
+
57
+ ## Introduction
58
+ Brief intro to what this codebase does and what a developer will learn from this guide.
59
+
60
+ ## Table of Contents
61
+ Numbered list of sections based on the topics covered in the Q&A.
62
+
63
+ ## [Section for each major topic covered]
64
+ Reorganize and expand the Q&A content into coherent sections with:
65
+ - Clear explanations
66
+ - Code examples (preserve the file:line citations)
67
+ - Key takeaways at the end of each section
68
+
69
+ ## Summary
70
+ What a developer now knows after reading this guide and next steps.
71
+
72
+ Write the course document only. Make it genuinely useful for onboarding a new developer.`;
73
+ console.log("\nGenerating course document...");
74
+ const courseContent = await (0, lib_1.generateText)("You are an expert technical writer creating developer onboarding documentation.", prompt, 4096);
75
+ fs.writeFileSync(outputPath, courseContent, "utf-8");
76
+ console.log(`Course exported to: ${outputPath}`);
77
+ }
78
+ //# sourceMappingURL=course.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const lib_1 = require("./lib");
6
+ const session_1 = require("./session");
7
+ const course_1 = require("./course");
8
+ const program = new commander_1.Command();
9
+ program
10
+ .name("claude-teach")
11
+ .description("Interactive Q&A about any codebase, powered by Claude")
12
+ .version("0.1.0")
13
+ .argument("<source>", "GitHub URL or local path")
14
+ .option("--export <file>", "Export session as a course document when you quit")
15
+ .option("--no-stream", "Disable streaming output")
16
+ .option("--github-token <token>", "GitHub token for private/large repos")
17
+ .action(async (source, opts) => {
18
+ try {
19
+ console.log(`Loading repository: ${source}`);
20
+ const isGitHub = source.startsWith("https://github.com") ||
21
+ source.startsWith("github.com") ||
22
+ /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+$/.test(source);
23
+ const ctx = isGitHub
24
+ ? await (0, lib_1.readGitHubRepo)(source, opts.githubToken ?? (0, lib_1.resolveGitHubToken)())
25
+ : await (0, lib_1.readLocalRepo)(source);
26
+ const turns = await (0, session_1.runSession)(ctx, opts.export, opts.stream !== false);
27
+ if (opts.export && turns.length > 0) {
28
+ await (0, course_1.exportCourse)(ctx, turns, opts.export);
29
+ }
30
+ console.log("Session ended.");
31
+ }
32
+ catch (err) {
33
+ if (err.code === "ERR_USE_AFTER_CLOSE") {
34
+ process.exit(0);
35
+ }
36
+ console.error("Error:", err instanceof Error ? err.message : String(err));
37
+ process.exit(1);
38
+ }
39
+ });
40
+ program.parse();
41
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ export declare function resolveAnthropicKey(): string;
2
+ export declare function resolveGitHubToken(): string | undefined;
3
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.resolveAnthropicKey = resolveAnthropicKey;
37
+ exports.resolveGitHubToken = resolveGitHubToken;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const os = __importStar(require("os"));
41
+ function readEnvFile(filePath) {
42
+ if (!fs.existsSync(filePath))
43
+ return {};
44
+ const lines = fs.readFileSync(filePath, "utf-8").split("\n");
45
+ const result = {};
46
+ for (const line of lines) {
47
+ const trimmed = line.trim();
48
+ if (!trimmed || trimmed.startsWith("#"))
49
+ continue;
50
+ const eq = trimmed.indexOf("=");
51
+ if (eq === -1)
52
+ continue;
53
+ const key = trimmed.slice(0, eq).trim();
54
+ const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
55
+ result[key] = val;
56
+ }
57
+ return result;
58
+ }
59
+ function resolveAnthropicKey() {
60
+ if (process.env.ANTHROPIC_API_KEY)
61
+ return process.env.ANTHROPIC_API_KEY;
62
+ const envFile = readEnvFile(path.join(process.cwd(), ".env"));
63
+ if (envFile.ANTHROPIC_API_KEY)
64
+ return envFile.ANTHROPIC_API_KEY;
65
+ const credFile = path.join(os.homedir(), ".anthropic", "credentials");
66
+ if (fs.existsSync(credFile)) {
67
+ const creds = readEnvFile(credFile);
68
+ if (creds.ANTHROPIC_API_KEY)
69
+ return creds.ANTHROPIC_API_KEY;
70
+ }
71
+ throw new Error("ANTHROPIC_API_KEY not found.\n" +
72
+ "Set it via:\n" +
73
+ " export ANTHROPIC_API_KEY=sk-ant-...\n" +
74
+ " or add it to .env in your current directory");
75
+ }
76
+ function resolveGitHubToken() {
77
+ if (process.env.GITHUB_TOKEN)
78
+ return process.env.GITHUB_TOKEN;
79
+ const envFile = readEnvFile(path.join(process.cwd(), ".env"));
80
+ if (envFile.GITHUB_TOKEN)
81
+ return envFile.GITHUB_TOKEN;
82
+ try {
83
+ const { execSync } = require("child_process");
84
+ const token = execSync("gh auth token 2>/dev/null", { encoding: "utf-8" }).trim();
85
+ if (token)
86
+ return token;
87
+ }
88
+ catch {
89
+ // gh CLI not available or not logged in
90
+ }
91
+ return undefined;
92
+ }
93
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,11 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ export declare function generateText(system: string, prompt: string, maxTokens?: number): Promise<string>;
3
+ export declare function streamText(system: string, prompt: string, maxTokens?: number): AsyncGenerator<string>;
4
+ export type MessageParam = Anthropic.MessageParam;
5
+ export type Tool = Anthropic.Tool;
6
+ export interface ToolLoopResult {
7
+ finalText: string;
8
+ iterations: number;
9
+ }
10
+ export declare function runToolLoop(system: string, initialPrompt: string, tools: Tool[], toolExecutor: (name: string, input: Record<string, unknown>) => Promise<unknown>, maxIterations?: number): Promise<ToolLoopResult>;
11
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateText = generateText;
7
+ exports.streamText = streamText;
8
+ exports.runToolLoop = runToolLoop;
9
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
10
+ const auth_1 = require("./auth");
11
+ let _client = null;
12
+ function getClient() {
13
+ if (!_client) {
14
+ _client = new sdk_1.default({ apiKey: (0, auth_1.resolveAnthropicKey)() });
15
+ }
16
+ return _client;
17
+ }
18
+ const DEFAULT_MODEL = "claude-opus-4-5";
19
+ async function generateText(system, prompt, maxTokens = 4096) {
20
+ const client = getClient();
21
+ const response = await client.messages.create({
22
+ model: DEFAULT_MODEL,
23
+ max_tokens: maxTokens,
24
+ system,
25
+ messages: [{ role: "user", content: prompt }],
26
+ });
27
+ const block = response.content[0];
28
+ if (block.type !== "text")
29
+ throw new Error("Unexpected response type");
30
+ return block.text;
31
+ }
32
+ async function* streamText(system, prompt, maxTokens = 4096) {
33
+ const client = getClient();
34
+ const stream = client.messages.stream({
35
+ model: DEFAULT_MODEL,
36
+ max_tokens: maxTokens,
37
+ system,
38
+ messages: [{ role: "user", content: prompt }],
39
+ });
40
+ for await (const event of stream) {
41
+ if (event.type === "content_block_delta" &&
42
+ event.delta.type === "text_delta") {
43
+ yield event.delta.text;
44
+ }
45
+ }
46
+ }
47
+ async function runToolLoop(system, initialPrompt, tools, toolExecutor, maxIterations = 20) {
48
+ const client = getClient();
49
+ const messages = [
50
+ { role: "user", content: initialPrompt },
51
+ ];
52
+ let iterations = 0;
53
+ let finalText = "";
54
+ while (iterations < maxIterations) {
55
+ iterations++;
56
+ const response = await client.messages.create({
57
+ model: DEFAULT_MODEL,
58
+ max_tokens: 8096,
59
+ system,
60
+ tools,
61
+ messages,
62
+ });
63
+ const assistantContent = response.content;
64
+ messages.push({ role: "assistant", content: assistantContent });
65
+ if (response.stop_reason === "end_turn") {
66
+ const textBlock = assistantContent.find((b) => b.type === "text");
67
+ if (textBlock && textBlock.type === "text")
68
+ finalText = textBlock.text;
69
+ break;
70
+ }
71
+ if (response.stop_reason === "tool_use") {
72
+ const toolResults = [];
73
+ for (const block of assistantContent) {
74
+ if (block.type !== "tool_use")
75
+ continue;
76
+ try {
77
+ const result = await toolExecutor(block.name, block.input);
78
+ toolResults.push({
79
+ type: "tool_result",
80
+ tool_use_id: block.id,
81
+ content: typeof result === "string" ? result : JSON.stringify(result),
82
+ });
83
+ }
84
+ catch (err) {
85
+ toolResults.push({
86
+ type: "tool_result",
87
+ tool_use_id: block.id,
88
+ content: `Error: ${err instanceof Error ? err.message : String(err)}`,
89
+ is_error: true,
90
+ });
91
+ }
92
+ }
93
+ messages.push({ role: "user", content: toolResults });
94
+ continue;
95
+ }
96
+ break;
97
+ }
98
+ return { finalText, iterations };
99
+ }
100
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,3 @@
1
+ import type { RepoContext } from "./github-reader";
2
+ export declare function readLocalRepo(dirPath: string): Promise<RepoContext>;
3
+ //# sourceMappingURL=fs-reader.d.ts.map