Skip to content

Commit

Permalink
chore(claude): generate release notes
Browse files Browse the repository at this point in the history
  • Loading branch information
NateFerrero committed May 15, 2024
1 parent 97e0658 commit 78eafe1
Show file tree
Hide file tree
Showing 54 changed files with 48,631 additions and 356 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ node_modules
/api
/core
/errors
/serialization
/serialization
.claude_cache
.env
106 changes: 106 additions & 0 deletions ai-diff-summary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const claudeApiKey = "";

import * as fs from "fs";
import * as path from "path";
import { sendPromptToClaude } from "./claude-ai";

let totalCount = 0;

async function processFile(
filePath: string,
extension: string,
callback: (fileContent: string) => Promise<string>,
apiKey: string
) {
if (path.extname(filePath).toLowerCase() === extension) {
const fileContent = fs.readFileSync(filePath, "utf-8");
const prompt = await callback(fileContent);
const resultFilePath = `${filePath}.result.md`;

if (!fs.existsSync(resultFilePath)) {
console.log(`Processing ${filePath}...`);
try {
totalCount++;
if (totalCount > 4) {
console.log("SKIP " + filePath);
return;
}
const claudeResponse = await sendPromptToClaude(
`You are an AI assistant responsible for writing release notes. You'll be given a git diff of a TypeScript Node API, from that write detailed release notes that summarize the changes made in the diff, especially noting any external interface changes in detail. Use backticks to represent code blocks or property names in the output text. Each section of the release notes should have a level-4 header (####). Only write release notes that are relevant to a consumer of the package. Do not reference anything internal-only, do not reference file names. If there are no changes to a section, omit that section from the output. If there are general changes, include them at the start of the output. Relate the changes to the relevant section of the FlatfileClient API, if appropriate, as follows:
Accounts
Agents
Apps
Assistant
Auth
Cells
Commits
DataRetentionPolicies
Documents
Entitlements
Environments
Events
Files
Foreigndb
Guests
Jobs
Mapping
Records
Roles
Secrets
Sheets
Snapshots
Spaces
Users
Versions
Views
Workbooks`,
[{ role: "user", content: prompt }],
apiKey
);
fs.writeFileSync(resultFilePath, claudeResponse);
console.log(`Processed ${filePath} and saved the result to ${resultFilePath}`);
} catch (error) {
console.error(`Error processing ${filePath}: ${error}`);
}
} else {
console.log(`Result file ${resultFilePath} already exists, skipping...`);
}
}
}

async function processDirectory(
dirPath: string,
extension: string,
callback: (fileContent: string) => Promise<string>,
apiKey: string
) {
const files = fs.readdirSync(dirPath);
const promises: Promise<void>[] = [];

for (const file of files) {
const filePath = path.join(dirPath, file);
const stats = fs.statSync(filePath);

if (stats.isDirectory()) {
promises.push(processDirectory(filePath, extension, callback, apiKey));
} else {
promises.push(processFile(filePath, extension, callback, apiKey));
}
}

await Promise.all(promises);
}

// Usage example
const directoryPath = "./release-notes";
const fileExtension = ".txt";

const promptCallback = async (fileContent: string): Promise<string> => {
// Generate the prompt based on the file content
// This is just an example, you can modify it as needed
return `Generate a summary of changes for the following text diff: ${fileContent}`;
};

processDirectory(directoryPath, fileExtension, promptCallback, claudeApiKey).catch((error) =>
console.error("Error:", error)
);
97 changes: 97 additions & 0 deletions claude-ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { createHash } from "crypto";
import fs from "fs/promises";
import { join } from "path";

const CLAUDE_API_URL = "https://api.anthropic.com/v1/messages";

async function makeClaudeRequest(system: string, messages: ClaudeMessage[], apiKey: string) {
const headers = {
"Anthropic-Version": "2023-06-01",
"Content-Type": "application/json",
"X-API-Key": apiKey,
};

const body = JSON.stringify({
max_tokens: 4096,
// https://docs.anthropic.com/claude/docs/models-overview
// model: 'claude-3-opus-20240229',
model: "claude-3-sonnet-20240229",
// model: 'claude-3-haiku-20240307'
messages,
system,
});

try {
const response = await fetch(CLAUDE_API_URL, {
method: "POST",
headers,
body,
});

if (!response.ok) {
console.error(await response.text());
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();

let content = data.content?.[0]?.text;

if (!content || typeof content !== "string") {
throw new Error("No content found in the API response");
}

return content;
} catch (error) {
console.error("Error calling Claude API:", error);
throw error;
}
}

export interface ClaudeMessage {
role: "assistant" | "user";
content: string;
}

export async function sendPromptToClaude(
system: string,
messages: ClaudeMessage[],
apiKey: string,
useCache: boolean = true
): Promise<string> {
const cacheHash = createHash("sha256");
cacheHash.update(JSON.stringify({ system, messages }));
const cacheKey = cacheHash.digest("hex");
const cacheDirectory = join(process.cwd(), ".claude_cache");
await fs.mkdir(cacheDirectory, { recursive: true });
const cacheFile = join(cacheDirectory, `${cacheKey}.cache.txt`);
if (useCache) {
try {
const cacheStat = await fs.stat(cacheFile);
if (cacheStat.size > 0) {
return JSON.parse(
await fs.readFile(cacheFile, {
encoding: "utf-8",
})
);
}
} catch (e) {
// assume cache doesn't exist
}
}
const result = await sendPrompt(system, messages, apiKey);
await fs.writeFile(cacheFile, JSON.stringify(result), {
encoding: "utf-8",
});
return result;
}

async function sendPrompt(system: string, messages: ClaudeMessage[], apiKey: string): Promise<string> {
let content = await makeClaudeRequest(system, messages, apiKey);

if (!content || typeof content !== "string") {
throw new Error("No content found in the API response");
}

return content;
}
39 changes: 39 additions & 0 deletions gen-diffs-script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fs from "node:fs";

const versions = [
"1.6.3",
"1.6.4",
"1.6.5",
"1.6.6",
"1.6.7",
"1.7.0",
"1.7.1",
"1.7.10",
"1.7.11",
"1.7.2",
"1.7.3",
"1.7.4",
"1.7.5",
"1.7.6",
"1.7.7",
"1.7.8",
"1.8.0",
"1.8.2",
"1.8.3",
"1.8.4",
"1.8.5",
"1.8.6",
"1.8.9",
];

let script: string[] = [];
let compareVersion = "1.6.2";

for (const version of versions) {
script.push(
`( git diff --no-color -U1 ${compareVersion}..${version} ':!yarn.lock' ) | grep -v '"X-' > release-notes/${version}.diff.txt`
);
compareVersion = version;
}

fs.writeFileSync("gen-diffs.sh", script.join("\n"));
23 changes: 23 additions & 0 deletions gen-diffs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
( git diff --no-color -U1 1.6.2..1.6.3 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.6.3.diff.txt
( git diff --no-color -U1 1.6.3..1.6.4 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.6.4.diff.txt
( git diff --no-color -U1 1.6.4..1.6.5 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.6.5.diff.txt
( git diff --no-color -U1 1.6.5..1.6.6 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.6.6.diff.txt
( git diff --no-color -U1 1.6.6..1.6.7 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.6.7.diff.txt
( git diff --no-color -U1 1.6.7..1.7.0 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.7.0.diff.txt
( git diff --no-color -U1 1.7.0..1.7.1 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.7.1.diff.txt
( git diff --no-color -U1 1.7.1..1.7.10 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.7.10.diff.txt
( git diff --no-color -U1 1.7.10..1.7.11 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.7.11.diff.txt
( git diff --no-color -U1 1.7.11..1.7.2 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.7.2.diff.txt
( git diff --no-color -U1 1.7.2..1.7.3 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.7.3.diff.txt
( git diff --no-color -U1 1.7.3..1.7.4 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.7.4.diff.txt
( git diff --no-color -U1 1.7.4..1.7.5 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.7.5.diff.txt
( git diff --no-color -U1 1.7.5..1.7.6 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.7.6.diff.txt
( git diff --no-color -U1 1.7.6..1.7.7 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.7.7.diff.txt
( git diff --no-color -U1 1.7.7..1.7.8 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.7.8.diff.txt
( git diff --no-color -U1 1.7.8..1.8.0 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.8.0.diff.txt
( git diff --no-color -U1 1.8.0..1.8.2 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.8.2.diff.txt
( git diff --no-color -U1 1.8.2..1.8.3 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.8.3.diff.txt
( git diff --no-color -U1 1.8.3..1.8.4 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.8.4.diff.txt
( git diff --no-color -U1 1.8.4..1.8.5 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.8.5.diff.txt
( git diff --no-color -U1 1.8.5..1.8.6 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.8.6.diff.txt
( git diff --no-color -U1 1.8.6..1.8.9 ':!yarn.lock' ) | grep -v '"X-' > release-notes/1.8.9.diff.txt
Loading

0 comments on commit 78eafe1

Please sign in to comment.