quackstack 1.0.23 → 1.0.25

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.
@@ -2,21 +2,48 @@
2
2
  import { client } from "../lib/database.js";
3
3
  import { localEmbeddings } from "../lib/local-embeddings.js";
4
4
  import { getAIClient } from "../lib/ai-provider.js";
5
- export async function search(query, projectName, provider, model) {
5
+ export async function search(query, projectName, provider, model, options = {}) {
6
+ const { boostRecent = false, boostFrequent = false, filterAuthor, recentDays = 30, } = options;
7
+ const whereClause = { projectName };
8
+ if (filterAuthor) {
9
+ whereClause.OR = [
10
+ { lastCommitEmail: filterAuthor },
11
+ { primaryAuthorEmail: filterAuthor },
12
+ ];
13
+ }
6
14
  const snippets = await client.codeSnippet.findMany({
7
- where: { projectName },
15
+ where: whereClause,
8
16
  });
9
17
  const allContent = snippets.map(s => s.content);
10
18
  localEmbeddings.addDocuments(allContent);
11
19
  const queryVector = localEmbeddings.getVector(query);
12
20
  const ranked = snippets
13
- .map(snippet => ({
14
- id: snippet.id,
15
- content: snippet.content,
16
- filePath: snippet.filePath,
17
- functionName: snippet.functionName,
18
- score: localEmbeddings.cosineSimilarity(queryVector, snippet.embedding),
19
- }))
21
+ .map(snippet => {
22
+ let score = localEmbeddings.cosineSimilarity(queryVector, snippet.embedding);
23
+ if (boostRecent && snippet.lastCommitDate) {
24
+ const daysSinceCommit = (Date.now() - snippet.lastCommitDate.getTime()) / (1000 * 60 * 60 * 24);
25
+ if (daysSinceCommit <= recentDays) {
26
+ const recencyBoost = 1 + (0.2 * (1 - daysSinceCommit / recentDays));
27
+ score *= recencyBoost;
28
+ }
29
+ }
30
+ if (boostFrequent && snippet.totalCommits) {
31
+ const commitBoost = 1 + (Math.min(snippet.totalCommits, 50) / 50) * 0.15;
32
+ score *= commitBoost;
33
+ }
34
+ return {
35
+ id: snippet.id,
36
+ content: snippet.content,
37
+ filePath: snippet.filePath,
38
+ functionName: snippet.functionName,
39
+ score,
40
+ lastCommitAuthor: snippet.lastCommitAuthor,
41
+ lastCommitDate: snippet.lastCommitDate,
42
+ lastCommitMessage: snippet.lastCommitMessage,
43
+ totalCommits: snippet.totalCommits,
44
+ primaryAuthor: snippet.primaryAuthor,
45
+ };
46
+ })
20
47
  .sort((a, b) => b.score - a.score);
21
48
  const seenFiles = new Set();
22
49
  const uniqueResults = ranked.filter(item => {
@@ -26,9 +53,86 @@ export async function search(query, projectName, provider, model) {
26
53
  return true;
27
54
  }).slice(0, 5);
28
55
  const context = uniqueResults
29
- .map((r, i) => `[${i + 1}] ${r.filePath}${r.functionName ? ` (${r.functionName})` : ""}\n${r.content}`)
56
+ .map((r, i) => {
57
+ let entry = `[${i + 1}] ${r.filePath}${r.functionName ? ` (${r.functionName})` : ""}`;
58
+ if (r.lastCommitAuthor || r.primaryAuthor) {
59
+ const author = r.primaryAuthor || r.lastCommitAuthor;
60
+ entry += `\nPrimary Author: ${author}`;
61
+ if (r.totalCommits) {
62
+ entry += ` (${r.totalCommits} commits)`;
63
+ }
64
+ }
65
+ if (r.lastCommitDate) {
66
+ const daysAgo = Math.floor((Date.now() - r.lastCommitDate.getTime()) / (1000 * 60 * 60 * 24));
67
+ entry += `\nLast Modified: ${daysAgo} days ago`;
68
+ if (r.lastCommitMessage) {
69
+ entry += `\nLast Commit: "${r.lastCommitMessage.substring(0, 60)}${r.lastCommitMessage.length > 60 ? '...' : ''}"`;
70
+ }
71
+ }
72
+ entry += `\n\n${r.content}`;
73
+ return entry;
74
+ })
30
75
  .join("\n\n---\n\n");
31
76
  const aiClient = getAIClient(provider, model);
32
- const answer = await aiClient.generateAnswer(query, context);
77
+ const enhancedPrompt = query +
78
+ "\n\nNote: Code snippets include git history metadata (authors, commit counts, last modified dates). " +
79
+ "Use this information to provide context about code ownership and recency when relevant.";
80
+ const answer = await aiClient.generateAnswer(enhancedPrompt, context);
33
81
  return { answer, sources: uniqueResults };
34
82
  }
83
+ export async function searchByAuthor(authorEmail, projectName) {
84
+ const snippets = await client.codeSnippet.findMany({
85
+ where: {
86
+ projectName,
87
+ OR: [
88
+ { lastCommitEmail: authorEmail },
89
+ { primaryAuthorEmail: authorEmail },
90
+ ],
91
+ },
92
+ select: {
93
+ filePath: true,
94
+ functionName: true,
95
+ totalCommits: true,
96
+ lastCommitDate: true,
97
+ },
98
+ orderBy: {
99
+ totalCommits: "desc",
100
+ },
101
+ });
102
+ const fileMap = new Map();
103
+ snippets.forEach(s => {
104
+ if (!fileMap.has(s.filePath)) {
105
+ fileMap.set(s.filePath, s);
106
+ }
107
+ });
108
+ return Array.from(fileMap.values());
109
+ }
110
+ export async function getRecentActivity(projectName, days = 7) {
111
+ const since = new Date();
112
+ since.setDate(since.getDate() - days);
113
+ const snippets = await client.codeSnippet.findMany({
114
+ where: {
115
+ projectName,
116
+ lastCommitDate: {
117
+ gte: since,
118
+ },
119
+ },
120
+ select: {
121
+ filePath: true,
122
+ lastCommitAuthor: true,
123
+ lastCommitDate: true,
124
+ lastCommitMessage: true,
125
+ },
126
+ orderBy: {
127
+ lastCommitDate: "desc",
128
+ },
129
+ take: 20,
130
+ });
131
+ const fileMap = new Map();
132
+ snippets.forEach(s => {
133
+ if (!fileMap.has(s.filePath)) {
134
+ fileMap.set(s.filePath, s);
135
+ }
136
+ });
137
+ return Array.from(fileMap.values());
138
+ }
@@ -1,9 +1,11 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import os from "os";
4
- import { search } from "../commands/search.js";
4
+ import { search, getRecentActivity } from "../commands/search.js";
5
+ import { getProjectAuthors } from "../lib/database.js";
6
+ import { gitHistory } from "../lib/git-history.js";
5
7
  export async function generateCodebaseDoc(projectName) {
6
- console.log("🔍 Analyzing your codebase...\n");
8
+ console.log("Analyzing your codebase...\n");
7
9
  const queries = [
8
10
  "What is the overall architecture and design patterns used?",
9
11
  "What are the main entry points and how does the application start?",
@@ -15,8 +17,24 @@ export async function generateCodebaseDoc(projectName) {
15
17
  let doc = `# ${projectName} - Codebase Documentation\n\n`;
16
18
  doc += `**Auto-generated by QuackStack** | Last updated: ${new Date().toLocaleString()}\n\n`;
17
19
  doc += `This document provides a high-level overview of the codebase architecture, key components, and design decisions.\n\n`;
18
- doc += `---\n\n`;
19
- doc += `## 📋 Table of Contents\n\n`;
20
+ if (gitHistory.isRepository()) {
21
+ doc += `## Repository Information\n\n`;
22
+ const currentBranch = gitHistory.getCurrentBranch();
23
+ if (currentBranch) {
24
+ doc += `**Current Branch:** ${currentBranch}\n\n`;
25
+ }
26
+ const recentCommits = gitHistory.getRecentCommits(10);
27
+ if (recentCommits.length > 0) {
28
+ doc += `**Recent Activity:**\n`;
29
+ recentCommits.slice(0, 5).forEach(commit => {
30
+ const date = new Date(commit.date).toLocaleDateString();
31
+ doc += `- ${date} - ${commit.author}: ${commit.message.substring(0, 60)}${commit.message.length > 60 ? '...' : ''}\n`;
32
+ });
33
+ doc += `\n`;
34
+ }
35
+ doc += `---\n\n`;
36
+ }
37
+ doc += `## Table of Contents\n\n`;
20
38
  queries.forEach((q, i) => {
21
39
  const anchor = q.toLowerCase().replace(/[^a-z0-9]+/g, '-');
22
40
  doc += `${i + 1}. [${q}](#${anchor})\n`;
@@ -28,12 +46,15 @@ export async function generateCodebaseDoc(projectName) {
28
46
  doc += `## ${query}\n\n`;
29
47
  doc += `${answer}\n\n`;
30
48
  if (sources.length > 0) {
31
- doc += `### 📁 Key Files\n\n`;
49
+ doc += `### Key Files\n\n`;
32
50
  sources.slice(0, 5).forEach(s => {
33
51
  doc += `- \`${s.filePath}\``;
34
52
  if (s.functionName) {
35
53
  doc += ` - ${s.functionName}`;
36
54
  }
55
+ if (s.primaryAuthor) {
56
+ doc += ` (maintained by ${s.primaryAuthor})`;
57
+ }
37
58
  doc += `\n`;
38
59
  });
39
60
  doc += `\n`;
@@ -44,7 +65,28 @@ export async function generateCodebaseDoc(projectName) {
44
65
  console.error(`Error analyzing: ${query}`);
45
66
  }
46
67
  }
47
- doc += `## 📂 Project Structure\n\n`;
68
+ if (gitHistory.isRepository()) {
69
+ doc += `## Contributors\n\n`;
70
+ const authors = await getProjectAuthors(projectName);
71
+ if (authors.length > 0) {
72
+ doc += `This project has ${authors.length} contributor${authors.length > 1 ? 's' : ''}:\n\n`;
73
+ authors.slice(0, 10).forEach((author, i) => {
74
+ doc += `${i + 1}. **${author.author}** (${author.email})\n`;
75
+ doc += ` - ${author.totalCommits} commits\n`;
76
+ doc += ` - +${author.linesAdded} / -${author.linesRemoved} lines\n`;
77
+ if (author.recentActivity) {
78
+ const daysAgo = Math.floor((Date.now() - author.recentActivity.getTime()) / (1000 * 60 * 60 * 24));
79
+ doc += ` - Last active ${daysAgo} days ago\n`;
80
+ }
81
+ if (author.filesOwned.length > 0) {
82
+ doc += ` - Primary owner of ${author.filesOwned.length} files\n`;
83
+ }
84
+ doc += `\n`;
85
+ });
86
+ doc += `---\n\n`;
87
+ }
88
+ }
89
+ doc += `## Project Structure\n\n`;
48
90
  doc += '```\n';
49
91
  doc += await getProjectStructure(process.cwd());
50
92
  doc += '```\n\n';
@@ -59,7 +101,7 @@ export async function generateCodebaseDoc(projectName) {
59
101
  doc += `### Running the Project\n`;
60
102
  doc += `Refer to \`package.json\` scripts section for available commands.\n\n`;
61
103
  doc += `---\n\n`;
62
- doc += `## 🔄 Updating This Document\n\n`;
104
+ doc += `## Updating This Document\n\n`;
63
105
  doc += `This documentation is auto-generated. To regenerate:\n\n`;
64
106
  doc += '```bash\n';
65
107
  doc += 'quack --docs\n';
@@ -81,6 +123,34 @@ export async function generateContextFiles(projectName) {
81
123
  let baseContext = `# ${projectName} - Codebase Context\n\n`;
82
124
  baseContext += `Generated: ${new Date().toISOString()}\n\n`;
83
125
  baseContext += "This file is auto-generated by QuackStack to provide AI assistants with codebase context.\n\n";
126
+ if (gitHistory.isRepository()) {
127
+ baseContext += `## Git Repository Info\n\n`;
128
+ const branch = gitHistory.getCurrentBranch();
129
+ if (branch) {
130
+ baseContext += `**Branch:** ${branch}\n\n`;
131
+ }
132
+ const recentActivity = await getRecentActivity(projectName, 7);
133
+ if (recentActivity.length > 0) {
134
+ baseContext += `**Recent Changes (Last 7 days):**\n\n`;
135
+ recentActivity.slice(0, 5).forEach(item => {
136
+ const daysAgo = Math.floor((Date.now() - item.lastCommitDate.getTime()) / (1000 * 60 * 60 * 24));
137
+ baseContext += `- ${item.filePath}\n`;
138
+ baseContext += ` - Modified by ${item.lastCommitAuthor} ${daysAgo} days ago\n`;
139
+ if (item.lastCommitMessage) {
140
+ baseContext += ` - "${item.lastCommitMessage.substring(0, 60)}${item.lastCommitMessage.length > 60 ? '...' : ''}"\n`;
141
+ }
142
+ });
143
+ baseContext += `\n`;
144
+ }
145
+ const authors = await getProjectAuthors(projectName);
146
+ if (authors.length > 0) {
147
+ baseContext += `**Top Contributors:**\n\n`;
148
+ authors.slice(0, 5).forEach(author => {
149
+ baseContext += `- ${author.author} (${author.totalCommits} commits, owns ${author.filesOwned.length} files)\n`;
150
+ });
151
+ baseContext += `\n`;
152
+ }
153
+ }
84
154
  baseContext += "---\n\n";
85
155
  for (const query of queries) {
86
156
  try {
@@ -89,7 +159,11 @@ export async function generateContextFiles(projectName) {
89
159
  if (sources.length > 0) {
90
160
  baseContext += "**Key files:**\n";
91
161
  sources.slice(0, 3).forEach(s => {
92
- baseContext += `- ${s.filePath}\n`;
162
+ baseContext += `- ${s.filePath}`;
163
+ if (s.primaryAuthor) {
164
+ baseContext += ` (${s.primaryAuthor})`;
165
+ }
166
+ baseContext += `\n`;
93
167
  });
94
168
  baseContext += "\n";
95
169
  }
@@ -100,12 +174,28 @@ export async function generateContextFiles(projectName) {
100
174
  }
101
175
  baseContext += "---\n\n## Project Structure\n\n";
102
176
  baseContext += await getProjectStructure(process.cwd());
177
+ if (gitHistory.isRepository()) {
178
+ const authors = await getProjectAuthors(projectName);
179
+ if (authors.length > 0) {
180
+ baseContext += "\n---\n\n## Code Ownership & Expertise\n\n";
181
+ baseContext += "When making changes, consider consulting these experts:\n\n";
182
+ for (const author of authors.slice(0, 5)) {
183
+ if (author.filesOwned.length > 0) {
184
+ baseContext += `**${author.author}** - Expert in:\n`;
185
+ author.filesOwned.slice(0, 5).forEach(file => {
186
+ baseContext += `- ${file}\n`;
187
+ });
188
+ baseContext += `\n`;
189
+ }
190
+ }
191
+ }
192
+ }
103
193
  await generateCursorRules(baseContext);
104
194
  await generateWindsurfContext(baseContext);
105
195
  await generateClineContext(baseContext);
106
196
  await generateContinueContext(baseContext);
107
197
  await generateAiderContext(baseContext);
108
- console.log("\n Context files generated for:");
198
+ console.log("\n Context files generated for:");
109
199
  console.log(" - Cursor (.cursorrules)");
110
200
  console.log(" - Windsurf (.windsurfrules)");
111
201
  console.log(" - Cline (.clinerules)");
@@ -138,11 +228,9 @@ async function generateAiderContext(context) {
138
228
  const aiderConfig = `# Aider configuration with QuackStack context
139
229
  # Project: ${path.basename(process.cwd())}
140
230
 
141
- # Context file
142
231
  read:
143
232
  - .aider.context.md
144
233
 
145
- # Model settings
146
234
  model: gpt-4o-mini
147
235
  edit-format: whole
148
236
  `;
@@ -166,6 +254,12 @@ export async function updateGlobalContext(projectName) {
166
254
  topFiles: sources.slice(0, 5).map(s => s.filePath),
167
255
  lastUpdated: new Date().toISOString(),
168
256
  };
257
+ if (gitHistory.isRepository()) {
258
+ const branch = gitHistory.getCurrentBranch();
259
+ const authors = await getProjectAuthors(projectName);
260
+ contexts[projectName].gitBranch = branch;
261
+ contexts[projectName].contributors = authors.length;
262
+ }
169
263
  fs.writeFileSync(contextPath, JSON.stringify(contexts, null, 2), "utf-8");
170
264
  }
171
265
  export function watchAndUpdateContext(projectName) {
@@ -180,7 +274,7 @@ export function watchAndUpdateContext(projectName) {
180
274
  await generateContextFiles(projectName);
181
275
  }, 5000);
182
276
  });
183
- console.log("👀 Watching for file changes...");
277
+ console.log("Watching for file changes...");
184
278
  }
185
279
  async function getProjectStructure(dir, prefix = "", maxDepth = 3, currentDepth = 0) {
186
280
  if (currentDepth >= maxDepth)
@@ -1,41 +1,109 @@
1
1
  import { PrismaClient } from "@prisma/client";
2
2
  export const client = new PrismaClient();
3
- export const saveToDB = async (data) => {
4
- try {
5
- const result = await client.codeSnippet.create({
6
- data: {
7
- ...data,
8
- embedding: data.embedding,
3
+ export async function saveToDB(data) {
4
+ await client.codeSnippet.create({
5
+ data: {
6
+ content: data.content,
7
+ embedding: data.embedding,
8
+ filePath: data.filePath,
9
+ projectName: data.projectName,
10
+ language: data.language,
11
+ functionName: data.functionName,
12
+ lineStart: data.lineStart,
13
+ lineEnd: data.lineEnd,
14
+ lastCommitHash: data.lastCommitHash,
15
+ lastCommitAuthor: data.lastCommitAuthor,
16
+ lastCommitEmail: data.lastCommitEmail,
17
+ lastCommitDate: data.lastCommitDate,
18
+ lastCommitMessage: data.lastCommitMessage,
19
+ totalCommits: data.totalCommits,
20
+ primaryAuthor: data.primaryAuthor,
21
+ primaryAuthorEmail: data.primaryAuthorEmail,
22
+ fileOwnerCommits: data.fileOwnerCommits,
23
+ },
24
+ });
25
+ }
26
+ export async function saveAuthorToDB(data) {
27
+ await client.gitAuthor.upsert({
28
+ where: {
29
+ projectName_email: {
30
+ projectName: data.projectName,
31
+ email: data.email,
9
32
  },
10
- });
11
- return result;
12
- }
13
- catch (e) {
14
- if (e instanceof Error) {
15
- console.error(`Error saving to DB: ${e.message}`);
33
+ },
34
+ create: data,
35
+ update: {
36
+ author: data.author,
37
+ totalCommits: data.totalCommits,
38
+ linesAdded: data.linesAdded,
39
+ linesRemoved: data.linesRemoved,
40
+ recentActivity: data.recentActivity,
41
+ filesOwned: data.filesOwned,
42
+ },
43
+ });
44
+ }
45
+ export async function clearProject(projectName) {
46
+ await client.codeSnippet.deleteMany({
47
+ where: { projectName },
48
+ });
49
+ await client.gitAuthor.deleteMany({
50
+ where: { projectName },
51
+ });
52
+ }
53
+ export async function getProjectAuthors(projectName) {
54
+ return await client.gitAuthor.findMany({
55
+ where: { projectName },
56
+ orderBy: { totalCommits: "desc" },
57
+ });
58
+ }
59
+ export async function getAuthorFiles(projectName, authorEmail) {
60
+ const snippets = await client.codeSnippet.findMany({
61
+ where: {
62
+ projectName,
63
+ primaryAuthorEmail: authorEmail,
64
+ },
65
+ select: {
66
+ filePath: true,
67
+ functionName: true,
68
+ totalCommits: true,
69
+ },
70
+ orderBy: {
71
+ totalCommits: "desc",
72
+ },
73
+ });
74
+ const fileMap = new Map();
75
+ snippets.forEach(s => {
76
+ if (!fileMap.has(s.filePath)) {
77
+ fileMap.set(s.filePath, s);
16
78
  }
17
- else {
18
- console.error(`Unknown error saving to DB:`, e);
19
- }
20
- throw e;
21
- }
22
- };
23
- export const getFromDB = async (projectName) => {
24
- try {
25
- const results = await client.codeSnippet.findMany({
26
- where: {
27
- projectName: projectName
28
- }
29
- });
30
- return results;
31
- }
32
- catch (e) {
33
- if (e instanceof Error) {
34
- console.error(`Error fetching from DB: ${e.message}`);
35
- }
36
- else {
37
- console.error(`Unknown error fetching from DB:`, e);
79
+ });
80
+ return Array.from(fileMap.values());
81
+ }
82
+ export async function getRecentlyModifiedFiles(projectName, days = 7) {
83
+ const since = new Date();
84
+ since.setDate(since.getDate() - days);
85
+ const snippets = await client.codeSnippet.findMany({
86
+ where: {
87
+ projectName,
88
+ lastCommitDate: {
89
+ gte: since,
90
+ },
91
+ },
92
+ select: {
93
+ filePath: true,
94
+ lastCommitAuthor: true,
95
+ lastCommitDate: true,
96
+ lastCommitMessage: true,
97
+ },
98
+ orderBy: {
99
+ lastCommitDate: "desc",
100
+ },
101
+ });
102
+ const fileMap = new Map();
103
+ snippets.forEach(s => {
104
+ if (!fileMap.has(s.filePath)) {
105
+ fileMap.set(s.filePath, s);
38
106
  }
39
- throw e;
40
- }
41
- };
107
+ });
108
+ return Array.from(fileMap.values());
109
+ }