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.
@@ -0,0 +1,179 @@
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.readLocalRepo = readLocalRepo;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const child_process_1 = require("child_process");
40
+ const SKIP_DIRS = new Set([
41
+ "node_modules", ".git", "dist", "build", ".next", "__pycache__",
42
+ ".cache", "coverage", ".nyc_output", "vendor", ".turbo",
43
+ ]);
44
+ const SOURCE_EXTENSIONS = new Set([
45
+ ".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java",
46
+ ".rb", ".php", ".cs", ".cpp", ".c", ".h", ".swift", ".kt",
47
+ ".md", ".yaml", ".yml", ".toml", ".json", ".sh", ".sql",
48
+ ]);
49
+ const PRIORITY_FILES = new Set([
50
+ "package.json", "README.md", "README.rst", "README.txt",
51
+ "CLAUDE.md", "ARCHITECTURE.md", "Makefile", "Dockerfile",
52
+ "docker-compose.yml", "pyproject.toml", "go.mod", "Cargo.toml",
53
+ "tsconfig.json", ".env.example",
54
+ ]);
55
+ const MAX_FILE_SIZE = 100 * 1024;
56
+ const TOKEN_BUDGET = 80_000;
57
+ function walkDir(dir, base, results) {
58
+ let entries;
59
+ try {
60
+ entries = fs.readdirSync(dir, { withFileTypes: true });
61
+ }
62
+ catch {
63
+ return;
64
+ }
65
+ for (const entry of entries) {
66
+ if (SKIP_DIRS.has(entry.name))
67
+ continue;
68
+ const full = path.join(dir, entry.name);
69
+ const rel = path.relative(base, full);
70
+ if (entry.isDirectory()) {
71
+ walkDir(full, base, results);
72
+ }
73
+ else if (entry.isFile()) {
74
+ results.push(rel);
75
+ }
76
+ }
77
+ }
78
+ function buildTree(paths) {
79
+ const lines = [];
80
+ for (const p of paths.slice(0, 200)) {
81
+ const depth = p.split(path.sep).length - 1;
82
+ const indent = " ".repeat(depth);
83
+ lines.push(`${indent}${path.basename(p)}`);
84
+ }
85
+ return lines.join("\n");
86
+ }
87
+ function getGitLog(dir) {
88
+ try {
89
+ const out = (0, child_process_1.execSync)('git log --oneline --format="%h|%s|%ai|%an" -20', { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
90
+ return out
91
+ .trim()
92
+ .split("\n")
93
+ .filter(Boolean)
94
+ .map((line) => {
95
+ const [sha, message, date, ...authorParts] = line.split("|");
96
+ return { sha, message, date, author: authorParts.join("|") };
97
+ });
98
+ }
99
+ catch {
100
+ return [];
101
+ }
102
+ }
103
+ function detectLanguage(files) {
104
+ const extCount = {};
105
+ for (const f of files) {
106
+ const ext = path.extname(f);
107
+ if (ext)
108
+ extCount[ext] = (extCount[ext] ?? 0) + 1;
109
+ }
110
+ const sorted = Object.entries(extCount).sort((a, b) => b[1] - a[1]);
111
+ const extToLang = {
112
+ ".ts": "TypeScript", ".tsx": "TypeScript", ".js": "JavaScript",
113
+ ".py": "Python", ".go": "Go", ".rs": "Rust", ".java": "Java",
114
+ ".rb": "Ruby", ".php": "PHP",
115
+ };
116
+ return sorted.length > 0 ? (extToLang[sorted[0][0]] ?? sorted[0][0]) : "Unknown";
117
+ }
118
+ async function readLocalRepo(dirPath) {
119
+ const absPath = path.resolve(dirPath);
120
+ if (!fs.existsSync(absPath))
121
+ throw new Error(`Path not found: ${absPath}`);
122
+ const allPaths = [];
123
+ walkDir(absPath, absPath, allPaths);
124
+ const eligible = allPaths.filter((p) => {
125
+ const ext = path.extname(p);
126
+ const basename = path.basename(p);
127
+ const stat = fs.statSync(path.join(absPath, p));
128
+ if (stat.size > MAX_FILE_SIZE)
129
+ return false;
130
+ return PRIORITY_FILES.has(basename) || SOURCE_EXTENSIONS.has(ext);
131
+ });
132
+ eligible.sort((a, b) => {
133
+ const aPriority = PRIORITY_FILES.has(path.basename(a)) ? 0 : 1;
134
+ const bPriority = PRIORITY_FILES.has(path.basename(b)) ? 0 : 1;
135
+ if (aPriority !== bPriority)
136
+ return aPriority - bPriority;
137
+ return a.split(path.sep).length - b.split(path.sep).length;
138
+ });
139
+ const files = [];
140
+ let totalChars = 0;
141
+ let truncated = false;
142
+ for (const rel of eligible) {
143
+ if (totalChars >= TOKEN_BUDGET) {
144
+ truncated = true;
145
+ break;
146
+ }
147
+ try {
148
+ const content = fs.readFileSync(path.join(absPath, rel), "utf-8");
149
+ files.push({ path: rel, content, size: content.length });
150
+ totalChars += content.length;
151
+ }
152
+ catch {
153
+ // skip unreadable files
154
+ }
155
+ }
156
+ const repoName = path.basename(absPath);
157
+ let packageJson = {};
158
+ const pkgFile = files.find((f) => f.path === "package.json");
159
+ if (pkgFile) {
160
+ try {
161
+ packageJson = JSON.parse(pkgFile.content);
162
+ }
163
+ catch { /* ignore */ }
164
+ }
165
+ return {
166
+ owner: "local",
167
+ repo: repoName,
168
+ defaultBranch: "main",
169
+ description: packageJson.description ?? "",
170
+ stars: 0,
171
+ language: detectLanguage(allPaths),
172
+ files,
173
+ directoryTree: buildTree(allPaths),
174
+ languages: {},
175
+ recentCommits: getGitLog(absPath),
176
+ truncated,
177
+ };
178
+ }
179
+ //# sourceMappingURL=fs-reader.js.map
@@ -0,0 +1,26 @@
1
+ export interface RepoFile {
2
+ path: string;
3
+ content: string;
4
+ size: number;
5
+ }
6
+ export interface GitCommit {
7
+ sha: string;
8
+ message: string;
9
+ date: string;
10
+ author: string;
11
+ }
12
+ export interface RepoContext {
13
+ owner: string;
14
+ repo: string;
15
+ defaultBranch: string;
16
+ description: string;
17
+ stars: number;
18
+ language: string;
19
+ files: RepoFile[];
20
+ directoryTree: string;
21
+ languages: Record<string, number>;
22
+ recentCommits: GitCommit[];
23
+ truncated: boolean;
24
+ }
25
+ export declare function readGitHubRepo(input: string, githubToken?: string): Promise<RepoContext>;
26
+ //# sourceMappingURL=github-reader.d.ts.map
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readGitHubRepo = readGitHubRepo;
4
+ const rest_1 = require("@octokit/rest");
5
+ const SKIP_DIRS = new Set([
6
+ "node_modules", ".git", "dist", "build", ".next", "__pycache__",
7
+ ".cache", "coverage", ".nyc_output", "vendor", ".turbo",
8
+ ]);
9
+ const PRIORITY_FILES = new Set([
10
+ "package.json", "README.md", "README.rst", "README.txt",
11
+ "CLAUDE.md", "ARCHITECTURE.md", "Makefile", "Dockerfile",
12
+ "docker-compose.yml", "pyproject.toml", "go.mod", "Cargo.toml",
13
+ "tsconfig.json", ".env.example",
14
+ ]);
15
+ const SOURCE_EXTENSIONS = new Set([
16
+ ".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java",
17
+ ".rb", ".php", ".cs", ".cpp", ".c", ".h", ".swift", ".kt",
18
+ ".md", ".yaml", ".yml", ".toml", ".json", ".sh", ".sql",
19
+ ]);
20
+ const MAX_FILE_SIZE = 100 * 1024; // 100KB
21
+ const TOKEN_BUDGET = 80_000; // ~80K chars
22
+ function parseGitHubUrl(input) {
23
+ // Handle: https://github.com/owner/repo, github.com/owner/repo, owner/repo
24
+ const cleaned = input
25
+ .replace(/^https?:\/\//, "")
26
+ .replace(/^github\.com\//, "")
27
+ .replace(/\.git$/, "")
28
+ .split("#")[0]
29
+ .split("?")[0]
30
+ .trim();
31
+ const parts = cleaned.split("/").filter(Boolean);
32
+ if (parts.length < 2)
33
+ throw new Error(`Cannot parse GitHub URL: ${input}`);
34
+ return { owner: parts[0], repo: parts[1] };
35
+ }
36
+ function buildTree(paths) {
37
+ const tree = {};
38
+ for (const p of paths) {
39
+ const parts = p.split("/");
40
+ let node = tree;
41
+ for (let i = 0; i < parts.length; i++) {
42
+ const part = parts[i];
43
+ if (i === parts.length - 1) {
44
+ node[part] = null;
45
+ }
46
+ else {
47
+ if (!node[part])
48
+ node[part] = {};
49
+ node = node[part];
50
+ }
51
+ }
52
+ }
53
+ function render(node, indent = "") {
54
+ return Object.entries(node)
55
+ .map(([key, val]) => {
56
+ if (val === null)
57
+ return `${indent}${key}`;
58
+ return `${indent}${key}/\n${render(val, indent + " ")}`;
59
+ })
60
+ .join("\n");
61
+ }
62
+ return render(tree);
63
+ }
64
+ async function readGitHubRepo(input, githubToken) {
65
+ const { owner, repo } = parseGitHubUrl(input);
66
+ const octokit = new rest_1.Octokit({ auth: githubToken });
67
+ // Fetch repo metadata, tree, languages, and commits in parallel
68
+ const [repoData, langData, commitsData] = await Promise.all([
69
+ octokit.repos.get({ owner, repo }),
70
+ octokit.repos.listLanguages({ owner, repo }),
71
+ octokit.repos.listCommits({ owner, repo, per_page: 15 }),
72
+ ]);
73
+ const defaultBranch = repoData.data.default_branch;
74
+ // Get full file tree
75
+ const treeData = await octokit.git.getTree({
76
+ owner,
77
+ repo,
78
+ tree_sha: defaultBranch,
79
+ recursive: "1",
80
+ });
81
+ const allFiles = treeData.data.tree.filter((f) => f.type === "blob" && f.path);
82
+ // Filter out skipped directories and non-source files
83
+ const eligible = allFiles.filter((f) => {
84
+ const parts = f.path.split("/");
85
+ if (parts.some((p) => SKIP_DIRS.has(p)))
86
+ return false;
87
+ if ((f.size ?? 0) > MAX_FILE_SIZE)
88
+ return false;
89
+ const ext = "." + f.path.split(".").pop();
90
+ const basename = f.path.split("/").pop();
91
+ return PRIORITY_FILES.has(basename) || SOURCE_EXTENSIONS.has(ext);
92
+ });
93
+ // Sort: priority files first, then by path length (shorter = closer to root)
94
+ eligible.sort((a, b) => {
95
+ const aName = a.path.split("/").pop();
96
+ const bName = b.path.split("/").pop();
97
+ const aPriority = PRIORITY_FILES.has(aName) ? 0 : 1;
98
+ const bPriority = PRIORITY_FILES.has(bName) ? 0 : 1;
99
+ if (aPriority !== bPriority)
100
+ return aPriority - bPriority;
101
+ return a.path.split("/").length - b.path.split("/").length;
102
+ });
103
+ // Fetch file contents up to token budget
104
+ const files = [];
105
+ let totalChars = 0;
106
+ let truncated = false;
107
+ for (const file of eligible) {
108
+ if (totalChars >= TOKEN_BUDGET) {
109
+ truncated = true;
110
+ break;
111
+ }
112
+ try {
113
+ const contentData = await octokit.repos.getContent({
114
+ owner,
115
+ repo,
116
+ path: file.path,
117
+ });
118
+ const data = contentData.data;
119
+ if (!data.content || data.encoding !== "base64")
120
+ continue;
121
+ const decoded = Buffer.from(data.content, "base64").toString("utf-8");
122
+ files.push({ path: file.path, content: decoded, size: decoded.length });
123
+ totalChars += decoded.length;
124
+ }
125
+ catch {
126
+ // Skip files that fail to fetch
127
+ }
128
+ }
129
+ const recentCommits = commitsData.data.map((c) => ({
130
+ sha: c.sha.slice(0, 7),
131
+ message: c.commit.message.split("\n")[0],
132
+ date: c.commit.author?.date ?? "",
133
+ author: c.commit.author?.name ?? "unknown",
134
+ }));
135
+ return {
136
+ owner,
137
+ repo,
138
+ defaultBranch,
139
+ description: repoData.data.description ?? "",
140
+ stars: repoData.data.stargazers_count ?? 0,
141
+ language: repoData.data.language ?? "",
142
+ files,
143
+ directoryTree: buildTree(allFiles.map((f) => f.path).slice(0, 200)),
144
+ languages: langData.data,
145
+ recentCommits,
146
+ truncated,
147
+ };
148
+ }
149
+ //# sourceMappingURL=github-reader.js.map
@@ -0,0 +1,5 @@
1
+ export * from "./auth";
2
+ export * from "./client";
3
+ export * from "./github-reader";
4
+ export * from "./fs-reader";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,21 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./auth"), exports);
18
+ __exportStar(require("./client"), exports);
19
+ __exportStar(require("./github-reader"), exports);
20
+ __exportStar(require("./fs-reader"), exports);
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ import type { RepoContext } from "./lib";
2
+ export interface Turn {
3
+ question: string;
4
+ answer: string;
5
+ }
6
+ export declare function runSession(ctx: RepoContext, exportPath: string | undefined, stream: boolean): Promise<Turn[]>;
7
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1,139 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.runSession = runSession;
40
+ const readline = __importStar(require("readline"));
41
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
42
+ const lib_1 = require("./lib");
43
+ function buildSystemPrompt(ctx) {
44
+ const fileIndex = ctx.files.map((f) => `- ${f.path}`).join("\n");
45
+ const fileContents = ctx.files
46
+ .map((f) => {
47
+ const ext = f.path.split(".").pop() ?? "";
48
+ return `### ${f.path}\n\`\`\`${ext}\n${f.content.slice(0, 6000)}\n\`\`\``;
49
+ })
50
+ .join("\n\n");
51
+ return `You are an expert software engineer teaching a developer how the codebase "${ctx.owner}/${ctx.repo}" works.
52
+
53
+ ## About this repo
54
+ - Description: ${ctx.description || "N/A"}
55
+ - Primary language: ${ctx.language}
56
+ - Default branch: ${ctx.defaultBranch}
57
+
58
+ ## File Index
59
+ ${fileIndex}
60
+
61
+ ## File Contents
62
+ ${fileContents}
63
+
64
+ ${ctx.truncated ? "\n> Note: Repository was truncated to fit context window. Answer based on what is available." : ""}
65
+
66
+ ## Instructions
67
+ - Answer questions about this codebase specifically, based on the actual code above
68
+ - Always cite specific files and line numbers when referencing code, e.g. \`src/auth.ts:42\`
69
+ - If asked how something works, trace through the actual code step by step
70
+ - If you don't know something from the provided code, say so — don't guess
71
+ - Keep answers focused and practical
72
+ - Use markdown formatting for clarity`;
73
+ }
74
+ async function runSession(ctx, exportPath, stream) {
75
+ const client = new sdk_1.default({ apiKey: (0, lib_1.resolveAnthropicKey)() });
76
+ const systemPrompt = buildSystemPrompt(ctx);
77
+ const history = [];
78
+ const turns = [];
79
+ const rl = readline.createInterface({
80
+ input: process.stdin,
81
+ output: process.stdout,
82
+ terminal: true,
83
+ });
84
+ console.log(`\nLoaded ${ctx.files.length} files from ${ctx.owner}/${ctx.repo}`);
85
+ console.log('Ask questions about this codebase. Type "exit" or press Ctrl+C to quit.\n');
86
+ const ask = () => new Promise((resolve) => rl.question("You: ", resolve));
87
+ while (true) {
88
+ let question;
89
+ try {
90
+ question = (await ask()).trim();
91
+ }
92
+ catch {
93
+ break; // readline closed
94
+ }
95
+ if (!question)
96
+ continue;
97
+ if (question.toLowerCase() === "exit" || question.toLowerCase() === "quit")
98
+ break;
99
+ history.push({ role: "user", content: question });
100
+ process.stdout.write("\nClaude: ");
101
+ let answer = "";
102
+ if (stream) {
103
+ const streamResp = client.messages.stream({
104
+ model: "claude-opus-4-5",
105
+ max_tokens: 2048,
106
+ system: systemPrompt,
107
+ messages: history,
108
+ });
109
+ for await (const event of streamResp) {
110
+ if (event.type === "content_block_delta" &&
111
+ event.delta.type === "text_delta") {
112
+ process.stdout.write(event.delta.text);
113
+ answer += event.delta.text;
114
+ }
115
+ }
116
+ }
117
+ else {
118
+ const response = await client.messages.create({
119
+ model: "claude-opus-4-5",
120
+ max_tokens: 2048,
121
+ system: systemPrompt,
122
+ messages: history,
123
+ });
124
+ const block = response.content[0];
125
+ answer = block.type === "text" ? block.text : "";
126
+ process.stdout.write(answer);
127
+ }
128
+ console.log("\n");
129
+ history.push({ role: "assistant", content: answer });
130
+ turns.push({ question, answer });
131
+ // Trim history to last 20 turns to stay within context
132
+ if (history.length > 40) {
133
+ history.splice(0, 2);
134
+ }
135
+ }
136
+ rl.close();
137
+ return turns;
138
+ }
139
+ //# sourceMappingURL=session.js.map
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "claude-teach",
3
+ "version": "0.1.0",
4
+ "description": "Interactive Q&A about any codebase, powered by Claude",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "claude-teach": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -b",
11
+ "dev": "tsc -b --watch",
12
+ "start": "node dist/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@anthropic-ai/sdk": "^0.39.0",
16
+ "@octokit/rest": "^19.0.0",
17
+ "commander": "^12.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "^5.4.0"
21
+ }
22
+ }
package/src/course.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { generateText } from "./lib";
2
+ import type { RepoContext } from "./lib";
3
+ import type { Turn } from "./session";
4
+ import * as fs from "fs";
5
+
6
+ export async function exportCourse(
7
+ ctx: RepoContext,
8
+ turns: Turn[],
9
+ outputPath: string
10
+ ): Promise<void> {
11
+ if (turns.length === 0) {
12
+ console.log("No session turns to export.");
13
+ return;
14
+ }
15
+
16
+ const sessionTranscript = turns
17
+ .map((t, i) => `## Q${i + 1}: ${t.question}\n\n${t.answer}`)
18
+ .join("\n\n---\n\n");
19
+
20
+ const prompt = `You are creating a structured course document from a Q&A session about the codebase "${ctx.owner}/${ctx.repo}".
21
+
22
+ Here is the Q&A session transcript:
23
+
24
+ ${sessionTranscript}
25
+
26
+ Convert this into a well-structured course document with:
27
+
28
+ # ${ctx.owner}/${ctx.repo} — Codebase Guide
29
+
30
+ ## Introduction
31
+ Brief intro to what this codebase does and what a developer will learn from this guide.
32
+
33
+ ## Table of Contents
34
+ Numbered list of sections based on the topics covered in the Q&A.
35
+
36
+ ## [Section for each major topic covered]
37
+ Reorganize and expand the Q&A content into coherent sections with:
38
+ - Clear explanations
39
+ - Code examples (preserve the file:line citations)
40
+ - Key takeaways at the end of each section
41
+
42
+ ## Summary
43
+ What a developer now knows after reading this guide and next steps.
44
+
45
+ Write the course document only. Make it genuinely useful for onboarding a new developer.`;
46
+
47
+ console.log("\nGenerating course document...");
48
+ const courseContent = await generateText(
49
+ "You are an expert technical writer creating developer onboarding documentation.",
50
+ prompt,
51
+ 4096
52
+ );
53
+
54
+ fs.writeFileSync(outputPath, courseContent, "utf-8");
55
+ console.log(`Course exported to: ${outputPath}`);
56
+ }
package/src/index.ts ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { readGitHubRepo, readLocalRepo, resolveGitHubToken } from "./lib";
4
+ import { runSession } from "./session";
5
+ import { exportCourse } from "./course";
6
+
7
+ const program = new Command();
8
+
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: string, opts) => {
18
+ try {
19
+ console.log(`Loading repository: ${source}`);
20
+
21
+ const isGitHub =
22
+ source.startsWith("https://github.com") ||
23
+ source.startsWith("github.com") ||
24
+ /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+$/.test(source);
25
+
26
+ const ctx = isGitHub
27
+ ? await readGitHubRepo(source, opts.githubToken ?? resolveGitHubToken())
28
+ : await readLocalRepo(source);
29
+
30
+ const turns = await runSession(ctx, opts.export, opts.stream !== false);
31
+
32
+ if (opts.export && turns.length > 0) {
33
+ await exportCourse(ctx, turns, opts.export);
34
+ }
35
+
36
+ console.log("Session ended.");
37
+ } catch (err) {
38
+ if ((err as NodeJS.ErrnoException).code === "ERR_USE_AFTER_CLOSE") {
39
+ process.exit(0);
40
+ }
41
+ console.error("Error:", err instanceof Error ? err.message : String(err));
42
+ process.exit(1);
43
+ }
44
+ });
45
+
46
+ program.parse();