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 +21 -0
- package/README.md +184 -0
- package/dist/course.d.ts +4 -0
- package/dist/course.js +78 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +41 -0
- package/dist/lib/auth.d.ts +3 -0
- package/dist/lib/auth.js +93 -0
- package/dist/lib/client.d.ts +11 -0
- package/dist/lib/client.js +100 -0
- package/dist/lib/fs-reader.d.ts +3 -0
- package/dist/lib/fs-reader.js +179 -0
- package/dist/lib/github-reader.d.ts +26 -0
- package/dist/lib/github-reader.js +149 -0
- package/dist/lib/index.d.ts +5 -0
- package/dist/lib/index.js +21 -0
- package/dist/session.d.ts +7 -0
- package/dist/session.js +139 -0
- package/package.json +22 -0
- package/src/course.ts +56 -0
- package/src/index.ts +46 -0
- package/src/lib/auth.ts +56 -0
- package/src/lib/client.ts +123 -0
- package/src/lib/fs-reader.ts +147 -0
- package/src/lib/github-reader.ts +186 -0
- package/src/lib/index.ts +4 -0
- package/src/session.ts +124 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -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,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
|
package/dist/session.js
ADDED
|
@@ -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();
|