git-yapyap-cli 1.0.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/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # ๐Ÿ—ฃ๏ธ Git YapYap
2
+
3
+ > AI-powered GitHub activity summarizer โ€” see what you shipped, instantly.
4
+
5
+ Git YapYap fetches your GitHub commits for any date, groups them by repository, and generates incredibly concise AI-powered summaries of your daily work. Perfect for standups, daily logs, tracking what you actually accomplished, or just reflecting on your productivity.
6
+
7
+ ## โœจ Features
8
+ - **Fetch Past Commits**: Search any specific date (yesterday, today, or months ago).
9
+ - **Multi-Provider AI**: Use OpenAI, Google Gemini, Anthropic Claude, Groq, or Local LLMs to generate your summaries. The power is in your hands.
10
+ - **Private Repository Support**: With the right token scope, seamlessly summarize your private work without any fuss!
11
+ - **Fast & Interactive Setup**: 30-second interactive CLI configuration wizard.
12
+ - **Zero Configuration Daily Use**: Just type `yap yap` anywhere in your terminal.
13
+
14
+ ---
15
+
16
+ ## ๐Ÿš€ Installation & Distribution
17
+
18
+ Git YapYap is designed to be installed globally on your machine using Node.js.
19
+
20
+ ### Option A: Install direct from source (Local)
21
+ 1. Clone this repository or download the source code wrapper.
22
+ 2. Inside the project directory, install the dependencies and link it globally:
23
+ ```bash
24
+ npm install -g .
25
+ ```
26
+
27
+ ### Option B: Publishing to NPM (For worldwide release)
28
+ If you decide to release this to the world, log in to your NPM account and publish!
29
+ ```bash
30
+ npm login
31
+ npm publish
32
+ ```
33
+ Then, anyone can install your tool directly via NPM with:
34
+ ```bash
35
+ npm install -g git-yap
36
+ ```
37
+
38
+ ---
39
+
40
+ ## โš™๏ธ How to Setup (One-Time)
41
+
42
+ Once you install `git-yap`, you only need to run the setup wizard **once**.
43
+
44
+ ```bash
45
+ yap setup
46
+ ```
47
+
48
+ You will need two things to complete setup:
49
+ ### 1. GitHub Personal Access Token (PAT)
50
+ Git YapYap needs a GitHub token to securely fetch your commits.
51
+ 1. Go to your [GitHub Personal Access Tokens settings area](https://github.com/settings/tokens).
52
+ 2. Click **Generate new token (classic)**.
53
+ 3. In the Scopes section, **check the `repo` box**. *(This is required if you want it to summarize private repositories!)*
54
+ 4. Keep the token safe and paste it into the CLI wizard when asked.
55
+
56
+ ### 2. AI Platform Setup (API Key)
57
+ Pick your favorite LLM provider and grab a free API key:
58
+ - **Groq**: Free and blazingly fast `llama3` models. Get an API key at [console.groq.com](https://console.groq.com/keys).
59
+ - **Google Gemini**: Highly-capable `gemini-2.0-flash`. Get an API key at [aistudio.google.com](https://aistudio.google.com/app/apikey).
60
+ - **OpenAI**: The standard `gpt-4o-mini`. Get an API key at [platform.openai.com](https://platform.openai.com/api-keys).
61
+ - **Anthropic / Local**: Supported too!
62
+
63
+ ---
64
+
65
+ ## ๐Ÿ“– Usage
66
+
67
+ Using Git YapYap is as simple as it gets. You don't need to specify formats, just tell it when to yap.
68
+
69
+ ```bash
70
+ # Today's activity
71
+ yap yap
72
+
73
+ # Yesterday's activity
74
+ yap yesterday
75
+
76
+ # A specific date (DDMMYYYY, DD-MM-YYYY, or DD/MM/YYYY)
77
+ yap 16032026
78
+ yap 16-03-2026
79
+ yap 16/03/2026
80
+ ```
81
+
82
+ ### Example Summary Output:
83
+
84
+ ```text
85
+ ๐Ÿ“… Activity for Sunday, March 14, 2026
86
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
87
+ โœ” Fetched GitHub activity
88
+ โœ” Generated summaries
89
+
90
+ ๐Ÿ“ฆ abnv-o/Hypnohands (1 commit)
91
+ The Hypnohands repository received an update to include a meta pixel.
92
+ This addition enhances tracking capabilities for the project.
93
+
94
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
95
+ โœจ 1 repository ยท 1 commit
96
+ ```
97
+
98
+ ---
99
+
100
+ ## ๐Ÿ—๏ธ Version 1.0.0
101
+ This tool is robust, clean, and tested. Future versions may add local summary caching, output to markdown, and extra email filters.
102
+
103
+ **License**: MIT
package/bin/yap.js ADDED
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import chalk from "chalk";
5
+ import ora from "ora";
6
+ import { getConfig, isConfigured } from "../src/config.js";
7
+ import { runSetup } from "../src/setup.js";
8
+ import { parseDate } from "../src/dateParser.js";
9
+ import { fetchCommits } from "../src/github.js";
10
+ import { summarizeCommits } from "../src/summarizer.js";
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name("yap")
16
+ .description("๐Ÿ—ฃ๏ธ Git YapYap โ€” AI-powered GitHub activity summarizer")
17
+ .version("1.0.0");
18
+
19
+ // โ”€โ”€โ”€ Setup Command โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
20
+
21
+ program
22
+ .command("setup")
23
+ .description("Configure GitHub and AI credentials")
24
+ .action(async () => {
25
+ await runSetup();
26
+ });
27
+
28
+ // โ”€โ”€โ”€ Default Command (fetch & summarize) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
29
+
30
+ program
31
+ .argument(
32
+ "[date]",
33
+ "Date to fetch commits for (yap, yesterday, DDMMYYYY, DD-MM-YYYY, DD/MM/YYYY)",
34
+ )
35
+ .action(async (dateArg) => {
36
+ // Check if configured
37
+ if (!isConfigured()) {
38
+ console.log(chalk.yellow("\nโš ๏ธ Git YapYap is not configured yet.\n"));
39
+ console.log(
40
+ chalk.dim("Run ") +
41
+ chalk.cyan("yap setup") +
42
+ chalk.dim(" to get started.\n"),
43
+ );
44
+ process.exit(1);
45
+ }
46
+
47
+ const config = getConfig();
48
+
49
+ // Parse the date
50
+ let dateRange;
51
+ try {
52
+ dateRange = parseDate(dateArg);
53
+ } catch (error) {
54
+ console.log(chalk.red(`\nโŒ ${error.message}\n`));
55
+ process.exit(1);
56
+ }
57
+
58
+ // Header
59
+ console.log(chalk.bold.cyan(`\n๐Ÿ“… Activity for ${dateRange.displayDate}`));
60
+ console.log(chalk.dim("โ”€".repeat(45)));
61
+
62
+ // Fetch commits
63
+ const fetchSpinner = ora("Fetching GitHub activity...").start();
64
+ let commitsByRepo;
65
+ try {
66
+ commitsByRepo = await fetchCommits(
67
+ config.githubToken,
68
+ dateRange.since,
69
+ dateRange.until,
70
+ );
71
+ fetchSpinner.succeed(chalk.dim("Fetched GitHub activity"));
72
+ } catch (error) {
73
+ fetchSpinner.fail(chalk.red("Failed to fetch GitHub activity"));
74
+ console.log(chalk.red(`\n${error.message}\n`));
75
+ process.exit(1);
76
+ }
77
+
78
+ // Check if any commits found
79
+ const repoNames = Object.keys(commitsByRepo);
80
+ if (repoNames.length === 0) {
81
+ console.log(chalk.yellow("\n๐Ÿ˜ด No commits found for this date.\n"));
82
+ console.log(
83
+ chalk.dim(
84
+ "Either you took a day off (nice!) or try a different date.\n",
85
+ ),
86
+ );
87
+ process.exit(0);
88
+ }
89
+
90
+ // Count total commits
91
+ const totalCommits = repoNames.reduce(
92
+ (sum, repo) => sum + commitsByRepo[repo].length,
93
+ 0,
94
+ );
95
+
96
+ // Summarize each repo
97
+ const summarySpinner = ora("Generating AI summaries...").start();
98
+ const summaries = {};
99
+
100
+ for (const repo of repoNames) {
101
+ summarySpinner.text = `Summarizing ${chalk.cyan(repo)}...`;
102
+ summaries[repo] = await summarizeCommits(
103
+ config,
104
+ repo,
105
+ commitsByRepo[repo],
106
+ );
107
+ }
108
+ summarySpinner.succeed(chalk.dim("Generated summaries"));
109
+
110
+ // Output
111
+ console.log("");
112
+ for (const repo of repoNames) {
113
+ const commitCount = commitsByRepo[repo].length;
114
+ const commitWord = commitCount === 1 ? "commit" : "commits";
115
+ console.log(
116
+ chalk.bold(`๐Ÿ“ฆ ${repo}`) + chalk.dim(` (${commitCount} ${commitWord})`),
117
+ );
118
+
119
+ // Print raw commit messages
120
+ for (const commit of commitsByRepo[repo]) {
121
+ console.log(chalk.dim(` โ€ข ${commit.message}`));
122
+ }
123
+ console.log(""); // blank line between commits and summary
124
+
125
+ // Print AI summary
126
+ const summaryLines = summaries[repo].split("\n").filter((l) => l.trim());
127
+ for (const line of summaryLines) {
128
+ console.log(chalk.white(` ${line.trim()}`));
129
+ }
130
+ console.log("");
131
+ }
132
+
133
+ // Footer
134
+ console.log(chalk.dim("โ”€".repeat(45)));
135
+ const repoWord = repoNames.length === 1 ? "repository" : "repositories";
136
+ const commitWord = totalCommits === 1 ? "commit" : "commits";
137
+ console.log(
138
+ chalk.green(
139
+ `โœจ ${repoNames.length} ${repoWord} ยท ${totalCommits} ${commitWord}\n`,
140
+ ),
141
+ );
142
+ });
143
+
144
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "git-yapyap-cli",
3
+ "version": "1.0.0",
4
+ "description": "Git YapYap โ€” AI-powered GitHub activity summarizer. See what you shipped today.",
5
+ "type": "module",
6
+ "files": [
7
+ "bin",
8
+ "src"
9
+ ],
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
13
+ "bin": {
14
+ "yap": "bin/yap.js"
15
+ },
16
+ "scripts": {
17
+ "start": "node bin/yap.js"
18
+ },
19
+ "keywords": [
20
+ "github",
21
+ "commits",
22
+ "ai",
23
+ "summary",
24
+ "cli",
25
+ "productivity"
26
+ ],
27
+ "author": "Abhinav",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "chalk": "^5.3.0",
31
+ "commander": "^12.1.0",
32
+ "conf": "^13.0.1",
33
+ "inquirer": "^9.3.7",
34
+ "octokit": "^4.1.2",
35
+ "openai": "^4.77.0",
36
+ "ora": "^8.1.1"
37
+ }
38
+ }
package/src/config.js ADDED
@@ -0,0 +1,55 @@
1
+ import Conf from "conf";
2
+
3
+ const config = new Conf({
4
+ projectName: "git-yap",
5
+ schema: {
6
+ githubToken: {
7
+ type: "string",
8
+ default: "",
9
+ },
10
+ aiProvider: {
11
+ type: "string",
12
+ enum: ["openai", "gemini", "claude", "groq", "local"],
13
+ default: "openai",
14
+ },
15
+ aiApiKey: {
16
+ type: "string",
17
+ default: "",
18
+ },
19
+ aiModel: {
20
+ type: "string",
21
+ default: "",
22
+ },
23
+ aiBaseUrl: {
24
+ type: "string",
25
+ default: "",
26
+ },
27
+ },
28
+ });
29
+
30
+ export function getConfig() {
31
+ return {
32
+ githubToken: config.get("githubToken"),
33
+ aiProvider: config.get("aiProvider"),
34
+ aiApiKey: config.get("aiApiKey"),
35
+ aiModel: config.get("aiModel"),
36
+ aiBaseUrl: config.get("aiBaseUrl"),
37
+ };
38
+ }
39
+
40
+ export function setConfig(key, value) {
41
+ config.set(key, value);
42
+ }
43
+
44
+ export function isConfigured() {
45
+ const cfg = getConfig();
46
+ return !!(
47
+ cfg.githubToken &&
48
+ cfg.aiProvider &&
49
+ (cfg.aiApiKey || cfg.aiProvider === "local")
50
+ );
51
+ }
52
+
53
+ export function clearConfig() {
54
+ config.clear();
55
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Parse a user-provided date string into { since, until } ISO timestamps
3
+ * covering the full day in the local timezone.
4
+ *
5
+ * Supported formats:
6
+ * "yap" / undefined โ†’ today
7
+ * "yesterday" โ†’ yesterday
8
+ * "16032026" โ†’ DDMMYYYY
9
+ * "16-03-2026" โ†’ DD-MM-YYYY
10
+ * "16/03/2026" โ†’ DD/MM/YYYY
11
+ */
12
+ export function parseDate(input) {
13
+ let targetDate;
14
+
15
+ if (!input || input.toLowerCase() === "yap") {
16
+ // Today
17
+ targetDate = new Date();
18
+ } else if (input.toLowerCase() === "yesterday") {
19
+ targetDate = new Date();
20
+ targetDate.setDate(targetDate.getDate() - 1);
21
+ } else if (/^\d{8}$/.test(input)) {
22
+ // DDMMYYYY
23
+ const day = parseInt(input.slice(0, 2), 10);
24
+ const month = parseInt(input.slice(2, 4), 10) - 1;
25
+ const year = parseInt(input.slice(4, 8), 10);
26
+ targetDate = new Date(year, month, day);
27
+ } else if (/^\d{1,2}[-/]\d{1,2}[-/]\d{4}$/.test(input)) {
28
+ // DD-MM-YYYY or DD/MM/YYYY
29
+ const parts = input.split(/[-/]/);
30
+ const day = parseInt(parts[0], 10);
31
+ const month = parseInt(parts[1], 10) - 1;
32
+ const year = parseInt(parts[2], 10);
33
+ targetDate = new Date(year, month, day);
34
+ } else {
35
+ throw new Error(
36
+ `Invalid date format: "${input}"\n` +
37
+ `Supported: yap, yesterday, DDMMYYYY, DD-MM-YYYY, DD/MM/YYYY`,
38
+ );
39
+ }
40
+
41
+ // Validate the parsed date
42
+ if (isNaN(targetDate.getTime())) {
43
+ throw new Error(`Could not parse date: "${input}"`);
44
+ }
45
+
46
+ // Start of day (local timezone)
47
+ const since = new Date(targetDate);
48
+ since.setHours(0, 0, 0, 0);
49
+
50
+ // End of day (local timezone)
51
+ const until = new Date(targetDate);
52
+ until.setHours(23, 59, 59, 999);
53
+
54
+ return {
55
+ since: since.toISOString(),
56
+ until: until.toISOString(),
57
+ displayDate: targetDate.toLocaleDateString("en-US", {
58
+ weekday: "long",
59
+ year: "numeric",
60
+ month: "long",
61
+ day: "numeric",
62
+ }),
63
+ };
64
+ }
package/src/github.js ADDED
@@ -0,0 +1,80 @@
1
+ import { Octokit } from "octokit";
2
+
3
+ /**
4
+ * Fetch all commits by the authenticated user for a given date range,
5
+ * grouped by repository.
6
+ *
7
+ * Uses the GitHub Events API (GET /users/{username}/events) to fetch
8
+ * PushEvent entries, then filters by date and extracts commits.
9
+ *
10
+ * @param {string} token - GitHub Personal Access Token
11
+ * @param {string} since - ISO timestamp (start of day)
12
+ * @param {string} until - ISO timestamp (end of day)
13
+ * @returns {Promise<Object>} - { "owner/repo": [{ sha, message, timestamp, url }] }
14
+ */
15
+ export async function fetchCommits(token, since, until) {
16
+ const octokit = new Octokit({ auth: token });
17
+
18
+ // Get authenticated user's login
19
+ const { data: user } = await octokit.rest.users.getAuthenticated();
20
+ const username = user.login;
21
+
22
+ const commitsByRepo = {};
23
+ let page = 1;
24
+ const perPage = 100;
25
+ let keepGoing = true;
26
+
27
+ // The since/until variables are already full ISO strings like "2026-03-13T18:30:00.000Z"
28
+ // Format them for the search query, replacing the ".000Z" with "Z" if necessary, though it works fine.
29
+ const query = `author:${username} author-date:${since}..${until}`;
30
+
31
+ while (keepGoing && page <= 5) {
32
+ // max 500 commits per day is plenty
33
+ const { data } = await octokit.rest.search.commits({
34
+ q: query,
35
+ per_page: perPage,
36
+ page,
37
+ });
38
+
39
+ if (!data.items || data.items.length === 0) {
40
+ break;
41
+ }
42
+
43
+ for (const item of data.items) {
44
+ const repoName = item.repository.full_name;
45
+ if (!commitsByRepo[repoName]) {
46
+ commitsByRepo[repoName] = [];
47
+ }
48
+
49
+ // Deduplicate by SHA just in case
50
+ const shortSha = item.sha.substring(0, 7);
51
+ const alreadyAdded = commitsByRepo[repoName].some(
52
+ (c) => c.sha === shortSha,
53
+ );
54
+
55
+ if (!alreadyAdded) {
56
+ commitsByRepo[repoName].push({
57
+ sha: shortSha,
58
+ message: item.commit.message.split("\n")[0], // First line only
59
+ timestamp: item.commit.author.date,
60
+ url: item.html_url,
61
+ });
62
+ }
63
+ }
64
+
65
+ if (data.items.length < perPage) {
66
+ keepGoing = false;
67
+ } else {
68
+ page++;
69
+ }
70
+ }
71
+
72
+ // Remove empty repos (though search API shouldn't yield empty bins)
73
+ for (const repo of Object.keys(commitsByRepo)) {
74
+ if (commitsByRepo[repo].length === 0) {
75
+ delete commitsByRepo[repo];
76
+ }
77
+ }
78
+
79
+ return commitsByRepo;
80
+ }
package/src/setup.js ADDED
@@ -0,0 +1,125 @@
1
+ import inquirer from "inquirer";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { Octokit } from "octokit";
5
+ import { setConfig } from "./config.js";
6
+
7
+ const PROVIDER_DEFAULTS = {
8
+ openai: { model: "gpt-4o-mini", baseUrl: "" },
9
+ gemini: { model: "gemini-2.0-flash", baseUrl: "" },
10
+ claude: { model: "claude-3-5-haiku-20241022", baseUrl: "" },
11
+ groq: {
12
+ model: "llama-3.3-70b-versatile",
13
+ baseUrl: "https://api.groq.com/openai/v1",
14
+ },
15
+ local: { model: "llama3", baseUrl: "http://localhost:11434/v1" },
16
+ };
17
+
18
+ export async function runSetup() {
19
+ console.log(chalk.bold.cyan("\n๐Ÿ”ง Git YapYap Setup\n"));
20
+ console.log(chalk.dim("Configure your GitHub and AI credentials.\n"));
21
+
22
+ // Step 1: GitHub Token
23
+ const { githubToken } = await inquirer.prompt([
24
+ {
25
+ type: "password",
26
+ name: "githubToken",
27
+ message: "GitHub Personal Access Token:",
28
+ mask: "*",
29
+ validate: (input) => input.length > 0 || "Token is required",
30
+ },
31
+ ]);
32
+
33
+ // Validate GitHub token
34
+ const spinner = ora("Validating GitHub token...").start();
35
+ try {
36
+ const octokit = new Octokit({ auth: githubToken });
37
+ const { data } = await octokit.rest.users.getAuthenticated();
38
+ spinner.succeed(chalk.green(`Authenticated as ${chalk.bold(data.login)}`));
39
+ } catch (error) {
40
+ spinner.fail(
41
+ chalk.red("Invalid GitHub token. Please check and try again."),
42
+ );
43
+ return;
44
+ }
45
+
46
+ // Step 2: AI Provider
47
+ const { aiProvider } = await inquirer.prompt([
48
+ {
49
+ type: "list",
50
+ name: "aiProvider",
51
+ message: "Choose your AI provider:",
52
+ choices: [
53
+ { name: "OpenAI", value: "openai" },
54
+ { name: "Google Gemini", value: "gemini" },
55
+ { name: "Anthropic Claude", value: "claude" },
56
+ { name: "Groq", value: "groq" },
57
+ { name: "Local / Custom (Ollama, LM Studio, etc.)", value: "local" },
58
+ ],
59
+ },
60
+ ]);
61
+
62
+ // Step 3: API Key
63
+ let aiApiKey = "";
64
+ if (aiProvider !== "local") {
65
+ const response = await inquirer.prompt([
66
+ {
67
+ type: "password",
68
+ name: "aiApiKey",
69
+ message: `${aiProvider.charAt(0).toUpperCase() + aiProvider.slice(1)} API Key:`,
70
+ mask: "*",
71
+ validate: (input) => input.length > 0 || "API key is required",
72
+ },
73
+ ]);
74
+ aiApiKey = response.aiApiKey;
75
+ } else {
76
+ const response = await inquirer.prompt([
77
+ {
78
+ type: "password",
79
+ name: "aiApiKey",
80
+ message: "API Key (leave empty if not needed):",
81
+ mask: "*",
82
+ },
83
+ ]);
84
+ aiApiKey = response.aiApiKey || "";
85
+ }
86
+
87
+ // Step 4: Model
88
+ const defaults = PROVIDER_DEFAULTS[aiProvider];
89
+ const { aiModel } = await inquirer.prompt([
90
+ {
91
+ type: "input",
92
+ name: "aiModel",
93
+ message: "Model name:",
94
+ default: defaults.model,
95
+ },
96
+ ]);
97
+
98
+ // Step 5: Base URL (for Groq / Local)
99
+ let aiBaseUrl = defaults.baseUrl;
100
+ if (aiProvider === "groq" || aiProvider === "local") {
101
+ const response = await inquirer.prompt([
102
+ {
103
+ type: "input",
104
+ name: "aiBaseUrl",
105
+ message: "API Base URL:",
106
+ default: defaults.baseUrl,
107
+ },
108
+ ]);
109
+ aiBaseUrl = response.aiBaseUrl;
110
+ }
111
+
112
+ // Save everything
113
+ setConfig("githubToken", githubToken);
114
+ setConfig("aiProvider", aiProvider);
115
+ setConfig("aiApiKey", aiApiKey);
116
+ setConfig("aiModel", aiModel);
117
+ setConfig("aiBaseUrl", aiBaseUrl);
118
+
119
+ console.log(chalk.bold.green("\nโœ… Setup complete! You're ready to go.\n"));
120
+ console.log(
121
+ chalk.dim("Try running: ") +
122
+ chalk.cyan("yap yap") +
123
+ chalk.dim(" to see today's activity.\n"),
124
+ );
125
+ }
@@ -0,0 +1,122 @@
1
+ import OpenAI from "openai";
2
+
3
+ /**
4
+ * Generate a 2-line summary for a repository's commits using the configured AI provider.
5
+ *
6
+ * Supports: openai, gemini, claude, groq, local (OpenAI-compatible)
7
+ *
8
+ * @param {Object} config - { aiProvider, aiApiKey, aiModel, aiBaseUrl }
9
+ * @param {string} repoName - Repository name (e.g., "owner/repo")
10
+ * @param {Array} commits - Array of { sha, message, timestamp }
11
+ * @returns {Promise<string>} - 2-line summary
12
+ */
13
+ export async function summarizeCommits(config, repoName, commits) {
14
+ const commitMessages = commits.map((c) => `- ${c.message}`).join("\n");
15
+
16
+ const prompt = `You are a concise technical writer. Summarize the following git commits for the repository "${repoName}" in exactly 2 short lines. Focus on what was accomplished overall, not individual commits. Be specific and informative.
17
+
18
+ Commits:
19
+ ${commitMessages}
20
+
21
+ Respond with exactly 2 lines, nothing else.`;
22
+
23
+ const { aiProvider, aiApiKey, aiModel, aiBaseUrl } = config;
24
+
25
+ try {
26
+ switch (aiProvider) {
27
+ case "openai":
28
+ return await callOpenAI(aiApiKey, aiModel, prompt);
29
+ case "groq":
30
+ case "local":
31
+ return await callOpenAICompatible(aiApiKey, aiModel, aiBaseUrl, prompt);
32
+ case "gemini":
33
+ return await callGemini(aiApiKey, aiModel, prompt);
34
+ case "claude":
35
+ return await callClaude(aiApiKey, aiModel, prompt);
36
+ default:
37
+ throw new Error(`Unknown AI provider: ${aiProvider}`);
38
+ }
39
+ } catch (error) {
40
+ return `โš ๏ธ Could not generate summary: ${error.message}`;
41
+ }
42
+ }
43
+
44
+ // โ”€โ”€โ”€ OpenAI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
45
+
46
+ async function callOpenAI(apiKey, model, prompt) {
47
+ const client = new OpenAI({ apiKey });
48
+ const response = await client.chat.completions.create({
49
+ model: model || "gpt-4o-mini",
50
+ messages: [{ role: "user", content: prompt }],
51
+ max_tokens: 150,
52
+ temperature: 0.3,
53
+ });
54
+ return response.choices[0].message.content.trim();
55
+ }
56
+
57
+ // โ”€โ”€โ”€ OpenAI-Compatible (Groq, Local/Ollama, LM Studio) โ”€โ”€โ”€โ”€โ”€โ”€
58
+
59
+ async function callOpenAICompatible(apiKey, model, baseUrl, prompt) {
60
+ const client = new OpenAI({
61
+ apiKey: apiKey || "not-needed",
62
+ baseURL: baseUrl,
63
+ });
64
+ const response = await client.chat.completions.create({
65
+ model: model || "llama3",
66
+ messages: [{ role: "user", content: prompt }],
67
+ max_tokens: 150,
68
+ temperature: 0.3,
69
+ });
70
+ return response.choices[0].message.content.trim();
71
+ }
72
+
73
+ // โ”€โ”€โ”€ Google Gemini (direct REST) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
74
+
75
+ async function callGemini(apiKey, model, prompt) {
76
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model || "gemini-2.0-flash"}:generateContent?key=${apiKey}`;
77
+ const response = await fetch(url, {
78
+ method: "POST",
79
+ headers: { "Content-Type": "application/json" },
80
+ body: JSON.stringify({
81
+ contents: [{ parts: [{ text: prompt }] }],
82
+ generationConfig: {
83
+ maxOutputTokens: 150,
84
+ temperature: 0.3,
85
+ },
86
+ }),
87
+ });
88
+
89
+ if (!response.ok) {
90
+ const error = await response.text();
91
+ throw new Error(`Gemini API error: ${response.status} ${error}`);
92
+ }
93
+
94
+ const data = await response.json();
95
+ return data.candidates[0].content.parts[0].text.trim();
96
+ }
97
+
98
+ // โ”€โ”€โ”€ Anthropic Claude (direct REST) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
99
+
100
+ async function callClaude(apiKey, model, prompt) {
101
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
102
+ method: "POST",
103
+ headers: {
104
+ "Content-Type": "application/json",
105
+ "x-api-key": apiKey,
106
+ "anthropic-version": "2023-06-01",
107
+ },
108
+ body: JSON.stringify({
109
+ model: model || "claude-3-5-haiku-20241022",
110
+ max_tokens: 150,
111
+ messages: [{ role: "user", content: prompt }],
112
+ }),
113
+ });
114
+
115
+ if (!response.ok) {
116
+ const error = await response.text();
117
+ throw new Error(`Claude API error: ${response.status} ${error}`);
118
+ }
119
+
120
+ const data = await response.json();
121
+ return data.content[0].text.trim();
122
+ }