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 +103 -0
- package/bin/yap.js +144 -0
- package/package.json +38 -0
- package/src/config.js +55 -0
- package/src/dateParser.js +64 -0
- package/src/github.js +80 -0
- package/src/setup.js +125 -0
- package/src/summarizer.js +122 -0
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
|
+
}
|