tarsk 0.2.5 → 0.3.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 +1 -7
- package/dist/index.d.ts +3 -0
- package/dist/index.js +92 -32
- package/dist/lib/response-builder.d.ts +50 -0
- package/dist/lib/response-builder.js +56 -0
- package/dist/lib/stream-helper.d.ts +39 -0
- package/dist/lib/stream-helper.js +43 -0
- package/dist/managers/ConversationManager.d.ts +83 -0
- package/dist/managers/ConversationManager.js +129 -0
- package/dist/managers/GitManager.d.ts +133 -0
- package/dist/managers/GitManager.js +330 -0
- package/dist/managers/MetadataManager.d.ts +139 -0
- package/dist/managers/MetadataManager.js +309 -0
- package/dist/managers/ModelManager.d.ts +57 -0
- package/dist/managers/ModelManager.js +129 -0
- package/dist/managers/NeovateExecutor.d.ts +40 -0
- package/dist/managers/NeovateExecutor.js +138 -0
- package/dist/managers/ProjectManager.d.ts +162 -0
- package/dist/managers/ProjectManager.js +353 -0
- package/dist/managers/ThreadManager.d.ts +181 -0
- package/dist/managers/ThreadManager.js +325 -0
- package/dist/managers/conversation-manager.d.ts +83 -0
- package/dist/managers/conversation-manager.js +129 -0
- package/dist/managers/git-manager.d.ts +133 -0
- package/dist/managers/git-manager.js +330 -0
- package/dist/managers/metadata-manager.d.ts +139 -0
- package/dist/managers/metadata-manager.js +305 -0
- package/dist/managers/model-manager.d.ts +59 -0
- package/dist/managers/model-manager.js +144 -0
- package/dist/managers/neovate-executor.d.ts +43 -0
- package/dist/managers/neovate-executor.js +205 -0
- package/dist/managers/processing-state-manager.d.ts +40 -0
- package/dist/managers/processing-state-manager.js +27 -0
- package/dist/managers/project-manager.d.ts +199 -0
- package/dist/managers/project-manager.js +465 -0
- package/dist/managers/thread-manager.d.ts +193 -0
- package/dist/managers/thread-manager.js +368 -0
- package/dist/model-info-aihubmix.d.ts +25 -0
- package/dist/model-info-aihubmix.js +117 -0
- package/dist/model-info-openai.d.ts +17 -0
- package/dist/model-info-openai.js +59 -0
- package/dist/model-info-openrouter.d.ts +25 -0
- package/dist/model-info-openrouter.js +101 -0
- package/dist/model-info.d.ts +37 -0
- package/dist/model-info.js +39 -0
- package/dist/provider-data.d.ts +101 -0
- package/dist/provider-data.js +471 -0
- package/dist/provider.d.ts +10 -0
- package/dist/provider.js +192 -0
- package/dist/public/android-chrome-192x192.png +0 -0
- package/dist/public/android-chrome-512x512.png +0 -0
- package/dist/public/apple-touch-icon.png +0 -0
- package/dist/public/assets/index-B443aj9k.js +8506 -0
- package/dist/public/assets/index-CjXGVbI7.css +1 -0
- package/dist/public/assets/index-DJC-p914.js +8506 -0
- package/dist/public/favicon-16x16.png +0 -0
- package/dist/public/favicon-32x32.png +0 -0
- package/dist/public/favicon.ico +0 -0
- package/dist/public/index.html +28 -0
- package/dist/public/manifest.json +82 -0
- package/dist/public/placeholder-logo.svg +1 -0
- package/dist/public/placeholder.svg +1 -0
- package/dist/public/snpro.woff2 +0 -0
- package/dist/public/tarsk-color.svg +12 -0
- package/dist/public/tarsk.png +0 -0
- package/dist/public/tarsk.svg +12 -0
- package/dist/public/zalando-sans.woff2 +0 -0
- package/dist/routes/chat-old.d.ts +21 -0
- package/dist/routes/chat-old.js +251 -0
- package/dist/routes/chat.d.ts +21 -0
- package/dist/routes/chat.js +217 -0
- package/dist/routes/git.d.ts +4 -0
- package/dist/routes/git.js +668 -0
- package/dist/routes/models.d.ts +18 -0
- package/dist/routes/models.js +128 -0
- package/dist/routes/projects-old.d.ts +20 -0
- package/dist/routes/projects-old.js +297 -0
- package/dist/routes/projects.d.ts +20 -0
- package/dist/routes/projects.js +365 -0
- package/dist/routes/providers.d.ts +15 -0
- package/dist/routes/providers.js +130 -0
- package/dist/routes/threads-old.d.ts +14 -0
- package/dist/routes/threads-old.js +393 -0
- package/dist/routes/threads.d.ts +14 -0
- package/dist/routes/threads.js +352 -0
- package/dist/types/models.d.ts +315 -0
- package/dist/types/models.js +11 -0
- package/dist/utils/env-manager.d.ts +3 -0
- package/dist/utils/env-manager.js +60 -0
- package/dist/utils/open-router-models.d.ts +45 -0
- package/dist/utils/open-router-models.js +103 -0
- package/dist/utils/openai-models.d.ts +63 -0
- package/dist/utils/openai-models.js +152 -0
- package/dist/utils/openai-pricing-scraper.d.ts +17 -0
- package/dist/utils/openai-pricing-scraper.js +185 -0
- package/dist/utils/validation.d.ts +10 -0
- package/dist/utils/validation.js +20 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +12 -0
- package/package.json +36 -22
- package/LICENSE.md +0 -7
- package/dist/agent/agent.js +0 -131
- package/dist/agent/interfaces.js +0 -1
- package/dist/api/encryption.js +0 -41
- package/dist/api/models.js +0 -169
- package/dist/api/prompt.js +0 -12
- package/dist/api/settings.js +0 -43
- package/dist/api/test.js +0 -29
- package/dist/api/tools.js +0 -287
- package/dist/api/utils.js +0 -18
- package/dist/interfaces/meta.js +0 -1
- package/dist/interfaces/model.js +0 -1
- package/dist/interfaces/settings.js +0 -1
- package/dist/log/log.js +0 -33
- package/dist/prompt.js +0 -49
- package/dist/tools.js +0 -84
- package/dist/utils/files.js +0 -14
- package/dist/utils/json-file.js +0 -28
- package/dist/utils/strip-markdown.js +0 -5
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { PROVIDERS } from '../provider.js';
|
|
5
|
+
import { createSession } from '@neovate/code';
|
|
6
|
+
// Helper function to generate commit message using AI
|
|
7
|
+
async function generateCommitMessageWithAI(diff, metadataManager, model, provider) {
|
|
8
|
+
// Truncate diff if too long (keep first 3000 chars)
|
|
9
|
+
const truncatedDiff = diff.length > 3000 ? diff.substring(0, 3000) + '\n...(truncated)' : diff;
|
|
10
|
+
const prompt = `Based on the following git diff, generate a concise, conventional commit message. Follow these guidelines:
|
|
11
|
+
- Use conventional commit format: type(scope): description
|
|
12
|
+
- Types: feat, fix, docs, style, refactor, test, chore
|
|
13
|
+
- Keep it under 72 characters
|
|
14
|
+
- Be specific and descriptive
|
|
15
|
+
- Focus on what changed and why
|
|
16
|
+
|
|
17
|
+
Git diff:
|
|
18
|
+
${truncatedDiff}
|
|
19
|
+
|
|
20
|
+
Generate only the commit message, nothing else:`;
|
|
21
|
+
let session = null;
|
|
22
|
+
try {
|
|
23
|
+
const providerKeys = await metadataManager.getProviderKeys();
|
|
24
|
+
let selectedProvider = null;
|
|
25
|
+
let apiKey = null;
|
|
26
|
+
let selectedModel = model;
|
|
27
|
+
// If provider is specified, use it
|
|
28
|
+
if (provider) {
|
|
29
|
+
selectedProvider = PROVIDERS.find(p => p.name.toLowerCase() === provider.toLowerCase()) || null;
|
|
30
|
+
if (selectedProvider) {
|
|
31
|
+
// Try environment variable first
|
|
32
|
+
if (selectedProvider.keyName) {
|
|
33
|
+
apiKey = process.env[selectedProvider.keyName] || null;
|
|
34
|
+
}
|
|
35
|
+
// Fall back to configured key
|
|
36
|
+
if (!apiKey) {
|
|
37
|
+
apiKey = providerKeys[selectedProvider.name] || null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// If no provider specified or provider not found, use priority order
|
|
42
|
+
if (!selectedProvider || !apiKey) {
|
|
43
|
+
const providerPriority = ['Anthropic', 'OpenAI', 'OpenRouter', 'Groq', 'DeepSeek'];
|
|
44
|
+
// First try environment variables for priority providers
|
|
45
|
+
for (const providerName of providerPriority) {
|
|
46
|
+
const prov = PROVIDERS.find(p => p.name === providerName);
|
|
47
|
+
if (prov?.keyName) {
|
|
48
|
+
const envKey = process.env[prov.keyName];
|
|
49
|
+
if (envKey) {
|
|
50
|
+
selectedProvider = prov;
|
|
51
|
+
apiKey = envKey;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// If no env key found, try configured keys
|
|
57
|
+
if (!selectedProvider || !apiKey) {
|
|
58
|
+
for (const providerName of providerPriority) {
|
|
59
|
+
const prov = PROVIDERS.find(p => p.name === providerName);
|
|
60
|
+
if (prov && providerKeys[prov.name]) {
|
|
61
|
+
selectedProvider = prov;
|
|
62
|
+
apiKey = providerKeys[prov.name];
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Fallback to any available provider
|
|
68
|
+
if (!selectedProvider || !apiKey) {
|
|
69
|
+
for (const prov of PROVIDERS) {
|
|
70
|
+
if (prov.api && prov.keyName) {
|
|
71
|
+
const envKey = process.env[prov.keyName];
|
|
72
|
+
if (envKey) {
|
|
73
|
+
selectedProvider = prov;
|
|
74
|
+
apiKey = envKey;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
if (providerKeys[prov.name]) {
|
|
78
|
+
selectedProvider = prov;
|
|
79
|
+
apiKey = providerKeys[prov.name];
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Select default model if not provided
|
|
86
|
+
if (!selectedModel) {
|
|
87
|
+
const modelMap = {
|
|
88
|
+
'Anthropic': 'claude-3-5-sonnet-20241022',
|
|
89
|
+
'OpenAI': 'gpt-4o-mini',
|
|
90
|
+
'OpenRouter': 'anthropic/claude-3.5-sonnet',
|
|
91
|
+
'Groq': 'llama-3.3-70b-versatile',
|
|
92
|
+
'DeepSeek': 'deepseek-chat',
|
|
93
|
+
};
|
|
94
|
+
selectedModel = modelMap[selectedProvider?.name || ''] || 'default';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!selectedProvider || !apiKey || !selectedProvider.api) {
|
|
98
|
+
throw new Error('No AI provider configured. Please configure an API key in settings.');
|
|
99
|
+
}
|
|
100
|
+
// Remove provider prefix from model if present
|
|
101
|
+
if (selectedModel && selectedProvider) {
|
|
102
|
+
selectedModel = selectedModel.replace(`${selectedProvider.name.toLowerCase()}/`, '');
|
|
103
|
+
}
|
|
104
|
+
const finalModel = selectedModel || 'default';
|
|
105
|
+
const providers = {
|
|
106
|
+
tarsk: {
|
|
107
|
+
api: selectedProvider.api,
|
|
108
|
+
options: { apiKey },
|
|
109
|
+
models: { [finalModel]: finalModel },
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const sessionConfig = {
|
|
113
|
+
model: `tarsk/${finalModel}`,
|
|
114
|
+
cwd: process.cwd(),
|
|
115
|
+
productName: 'Tarsk.io',
|
|
116
|
+
providers,
|
|
117
|
+
};
|
|
118
|
+
session = await createSession(sessionConfig);
|
|
119
|
+
await session.send(prompt);
|
|
120
|
+
let commitMessage = '';
|
|
121
|
+
for await (const msg of session.receive()) {
|
|
122
|
+
if (msg?.type === 'message' || msg?.type === 'result') {
|
|
123
|
+
const content = typeof msg.text === 'string' ? msg.text :
|
|
124
|
+
typeof msg.content === 'string' ? msg.content :
|
|
125
|
+
JSON.stringify(msg.content);
|
|
126
|
+
commitMessage = content.trim();
|
|
127
|
+
if (msg?.type === 'result')
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Clean up the message (remove quotes if present)
|
|
132
|
+
commitMessage = commitMessage.replace(/^["']|["']$/g, '').trim();
|
|
133
|
+
return commitMessage || 'Update files';
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error('Failed to generate AI commit message:', error);
|
|
137
|
+
// Fallback to a simple message
|
|
138
|
+
return 'Update files';
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
if (session) {
|
|
142
|
+
try {
|
|
143
|
+
await session.close?.();
|
|
144
|
+
}
|
|
145
|
+
catch (closeError) {
|
|
146
|
+
console.error('Error closing session:', closeError);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export function createGitRoutes(metadataManager) {
|
|
152
|
+
const router = new Hono();
|
|
153
|
+
// GET /api/git/username - returns the git user.name from local git config
|
|
154
|
+
router.get('/username', async (c) => {
|
|
155
|
+
try {
|
|
156
|
+
const name = await new Promise((resolve, reject) => {
|
|
157
|
+
const proc = spawn('git', ['config', 'user.name']);
|
|
158
|
+
let out = '';
|
|
159
|
+
let err = '';
|
|
160
|
+
proc.stdout.on('data', (d) => { out += d.toString(); });
|
|
161
|
+
proc.stderr.on('data', (d) => { err += d.toString(); });
|
|
162
|
+
proc.on('close', (code) => {
|
|
163
|
+
if (code === 0) {
|
|
164
|
+
resolve(out.trim());
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
reject(new Error(err.trim() || `git exited ${code}`));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
proc.on('error', (_e) => reject(new Error('Failed to run git config')));
|
|
171
|
+
});
|
|
172
|
+
return c.json({ name });
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Silently return empty name if git config fails
|
|
176
|
+
return c.json({ name: '' });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
// GET /api/git/status/:threadId - returns git status for a thread
|
|
180
|
+
router.get('/status/:threadId', async (c) => {
|
|
181
|
+
try {
|
|
182
|
+
const threadId = c.req.param('threadId');
|
|
183
|
+
const thread = await metadataManager.loadThreads().then(threads => threads.find(t => t.id === threadId));
|
|
184
|
+
if (!thread) {
|
|
185
|
+
return c.json({ error: 'Thread not found' }, 404);
|
|
186
|
+
}
|
|
187
|
+
const repoPath = thread.path;
|
|
188
|
+
if (!repoPath) {
|
|
189
|
+
return c.json({ error: 'Thread path not found' }, 404);
|
|
190
|
+
}
|
|
191
|
+
// Resolve absolute path - thread.path is already relative to process.cwd()
|
|
192
|
+
const absolutePath = resolve(repoPath);
|
|
193
|
+
// Check for uncommitted changes and count files
|
|
194
|
+
const { hasChanges, changedFilesCount } = await new Promise((resolve) => {
|
|
195
|
+
const proc = spawn('git', ['status', '--porcelain'], { cwd: absolutePath });
|
|
196
|
+
let out = '';
|
|
197
|
+
proc.stdout.on('data', (d) => { out += d.toString(); });
|
|
198
|
+
proc.on('close', () => {
|
|
199
|
+
const lines = out.trim().split('\n').filter(line => line.length > 0);
|
|
200
|
+
resolve({
|
|
201
|
+
hasChanges: lines.length > 0,
|
|
202
|
+
changedFilesCount: lines.length
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
proc.on('error', () => resolve({ hasChanges: false, changedFilesCount: 0 }));
|
|
206
|
+
});
|
|
207
|
+
// Get current branch
|
|
208
|
+
const currentBranch = await new Promise((resolve) => {
|
|
209
|
+
const proc = spawn('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: absolutePath });
|
|
210
|
+
let out = '';
|
|
211
|
+
proc.stdout.on('data', (d) => { out += d.toString(); });
|
|
212
|
+
proc.on('close', () => {
|
|
213
|
+
resolve(out.trim());
|
|
214
|
+
});
|
|
215
|
+
proc.on('error', () => resolve(''));
|
|
216
|
+
});
|
|
217
|
+
// Check if there are unpushed commits
|
|
218
|
+
const hasUnpushedCommits = await new Promise((resolve) => {
|
|
219
|
+
const proc = spawn('git', ['log', `origin/${currentBranch}..HEAD`, '--oneline'], { cwd: absolutePath });
|
|
220
|
+
let out = '';
|
|
221
|
+
proc.stdout.on('data', (d) => { out += d.toString(); });
|
|
222
|
+
proc.on('close', () => {
|
|
223
|
+
resolve(out.trim().length > 0);
|
|
224
|
+
});
|
|
225
|
+
proc.on('error', () => resolve(false));
|
|
226
|
+
});
|
|
227
|
+
return c.json({
|
|
228
|
+
hasChanges,
|
|
229
|
+
changedFilesCount,
|
|
230
|
+
currentBranch,
|
|
231
|
+
hasUnpushedCommits
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return c.json({ error: 'Failed to get git status' }, 500);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
// POST /api/git/generate-commit-message/:threadId - generates AI commit message
|
|
239
|
+
router.post('/generate-commit-message/:threadId', async (c) => {
|
|
240
|
+
try {
|
|
241
|
+
const threadId = c.req.param('threadId');
|
|
242
|
+
const body = await c.req.json().catch(() => ({}));
|
|
243
|
+
const { model, provider } = body;
|
|
244
|
+
const thread = await metadataManager.loadThreads().then(threads => threads.find(t => t.id === threadId));
|
|
245
|
+
if (!thread) {
|
|
246
|
+
return c.json({ error: 'Thread not found' }, 404);
|
|
247
|
+
}
|
|
248
|
+
const repoPath = thread.path;
|
|
249
|
+
if (!repoPath) {
|
|
250
|
+
return c.json({ error: 'Thread path not found' }, 404);
|
|
251
|
+
}
|
|
252
|
+
// Resolve absolute path - thread.path is already relative to process.cwd()
|
|
253
|
+
const absolutePath = resolve(repoPath);
|
|
254
|
+
// Get git diff
|
|
255
|
+
const diff = await new Promise((resolve, reject) => {
|
|
256
|
+
const proc = spawn('git', ['diff', '--cached'], { cwd: absolutePath });
|
|
257
|
+
let out = '';
|
|
258
|
+
let err = '';
|
|
259
|
+
proc.stdout.on('data', (d) => { out += d.toString(); });
|
|
260
|
+
proc.stderr.on('data', (d) => { err += d.toString(); });
|
|
261
|
+
proc.on('close', (code) => {
|
|
262
|
+
if (code === 0) {
|
|
263
|
+
// If no staged changes, get unstaged diff
|
|
264
|
+
if (!out.trim()) {
|
|
265
|
+
const proc2 = spawn('git', ['diff'], { cwd: absolutePath });
|
|
266
|
+
let out2 = '';
|
|
267
|
+
proc2.stdout.on('data', (d) => { out2 += d.toString(); });
|
|
268
|
+
proc2.on('close', () => resolve(out2));
|
|
269
|
+
proc2.on('error', reject);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
resolve(out);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
reject(new Error(err || 'Failed to get git diff'));
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
proc.on('error', reject);
|
|
280
|
+
});
|
|
281
|
+
if (!diff.trim()) {
|
|
282
|
+
return c.json({ error: 'No changes to generate commit message for' }, 400);
|
|
283
|
+
}
|
|
284
|
+
// Use AI to generate commit message
|
|
285
|
+
const commitMessage = await generateCommitMessageWithAI(diff, metadataManager, model, provider);
|
|
286
|
+
return c.json({ message: commitMessage });
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
const message = error instanceof Error ? error.message : 'Failed to generate commit message';
|
|
290
|
+
return c.json({ error: message }, 500);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
// POST /api/git/commit/:threadId - commits changes
|
|
294
|
+
router.post('/commit/:threadId', async (c) => {
|
|
295
|
+
try {
|
|
296
|
+
const threadId = c.req.param('threadId');
|
|
297
|
+
const body = await c.req.json();
|
|
298
|
+
const { message } = body;
|
|
299
|
+
if (!message) {
|
|
300
|
+
return c.json({ error: 'Commit message is required' }, 400);
|
|
301
|
+
}
|
|
302
|
+
const thread = await metadataManager.loadThreads().then(threads => threads.find(t => t.id === threadId));
|
|
303
|
+
if (!thread) {
|
|
304
|
+
return c.json({ error: 'Thread not found' }, 404);
|
|
305
|
+
}
|
|
306
|
+
const repoPath = thread.path;
|
|
307
|
+
if (!repoPath) {
|
|
308
|
+
return c.json({ error: 'Thread path not found' }, 404);
|
|
309
|
+
}
|
|
310
|
+
// Resolve absolute path - thread.path is already relative to process.cwd()
|
|
311
|
+
const absolutePath = resolve(repoPath);
|
|
312
|
+
// Stage all changes
|
|
313
|
+
await new Promise((resolve, reject) => {
|
|
314
|
+
const proc = spawn('git', ['add', '-A'], { cwd: absolutePath });
|
|
315
|
+
proc.on('close', (code) => {
|
|
316
|
+
if (code === 0)
|
|
317
|
+
resolve();
|
|
318
|
+
else
|
|
319
|
+
reject(new Error('Failed to stage changes'));
|
|
320
|
+
});
|
|
321
|
+
proc.on('error', reject);
|
|
322
|
+
});
|
|
323
|
+
// Commit changes
|
|
324
|
+
await new Promise((resolve, reject) => {
|
|
325
|
+
const proc = spawn('git', ['commit', '-m', message], { cwd: absolutePath });
|
|
326
|
+
proc.on('close', (code) => {
|
|
327
|
+
if (code === 0)
|
|
328
|
+
resolve();
|
|
329
|
+
else
|
|
330
|
+
reject(new Error('Failed to commit changes'));
|
|
331
|
+
});
|
|
332
|
+
proc.on('error', reject);
|
|
333
|
+
});
|
|
334
|
+
return c.json({ success: true });
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
const message = error instanceof Error ? error.message : 'Failed to commit changes';
|
|
338
|
+
return c.json({ error: message }, 500);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
// POST /api/git/push/:threadId - pushes commits
|
|
342
|
+
router.post('/push/:threadId', async (c) => {
|
|
343
|
+
try {
|
|
344
|
+
const threadId = c.req.param('threadId');
|
|
345
|
+
const thread = await metadataManager.loadThreads().then(threads => threads.find(t => t.id === threadId));
|
|
346
|
+
if (!thread) {
|
|
347
|
+
return c.json({ error: 'Thread not found' }, 404);
|
|
348
|
+
}
|
|
349
|
+
const repoPath = thread.path;
|
|
350
|
+
if (!repoPath) {
|
|
351
|
+
return c.json({ error: 'Thread path not found' }, 404);
|
|
352
|
+
}
|
|
353
|
+
// Resolve absolute path - thread.path is already relative to process.cwd()
|
|
354
|
+
const absolutePath = resolve(repoPath);
|
|
355
|
+
// Get current branch
|
|
356
|
+
const currentBranch = await new Promise((resolve, reject) => {
|
|
357
|
+
const proc = spawn('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: absolutePath });
|
|
358
|
+
let out = '';
|
|
359
|
+
proc.stdout.on('data', (d) => { out += d.toString(); });
|
|
360
|
+
proc.on('close', () => resolve(out.trim()));
|
|
361
|
+
proc.on('error', reject);
|
|
362
|
+
});
|
|
363
|
+
// Push to origin
|
|
364
|
+
await new Promise((resolve, reject) => {
|
|
365
|
+
const proc = spawn('git', ['push', '-u', 'origin', currentBranch], { cwd: absolutePath });
|
|
366
|
+
let err = '';
|
|
367
|
+
proc.stderr.on('data', (d) => { err += d.toString(); });
|
|
368
|
+
proc.on('close', (code) => {
|
|
369
|
+
if (code === 0)
|
|
370
|
+
resolve();
|
|
371
|
+
else
|
|
372
|
+
reject(new Error(err || 'Failed to push changes'));
|
|
373
|
+
});
|
|
374
|
+
proc.on('error', reject);
|
|
375
|
+
});
|
|
376
|
+
return c.json({ success: true });
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
const message = error instanceof Error ? error.message : 'Failed to push changes';
|
|
380
|
+
return c.json({ error: message }, 500);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
// POST /api/git/generate-pr-info/:threadId - generates AI PR title and description
|
|
384
|
+
router.post('/generate-pr-info/:threadId', async (c) => {
|
|
385
|
+
try {
|
|
386
|
+
const threadId = c.req.param('threadId');
|
|
387
|
+
const body = await c.req.json().catch(() => ({}));
|
|
388
|
+
const { model, provider } = body;
|
|
389
|
+
const thread = await metadataManager.loadThreads().then(threads => threads.find(t => t.id === threadId));
|
|
390
|
+
if (!thread) {
|
|
391
|
+
return c.json({ error: 'Thread not found' }, 404);
|
|
392
|
+
}
|
|
393
|
+
const repoPath = thread.path;
|
|
394
|
+
if (!repoPath) {
|
|
395
|
+
return c.json({ error: 'Thread path not found' }, 404);
|
|
396
|
+
}
|
|
397
|
+
// Resolve absolute path - thread.path is already relative to process.cwd()
|
|
398
|
+
const absolutePath = resolve(repoPath);
|
|
399
|
+
// Get git diff - check both staged and unstaged changes
|
|
400
|
+
const diff = await new Promise((resolve, reject) => {
|
|
401
|
+
const proc = spawn('git', ['diff', 'HEAD'], { cwd: absolutePath });
|
|
402
|
+
let out = '';
|
|
403
|
+
let err = '';
|
|
404
|
+
proc.stdout.on('data', (d) => { out += d.toString(); });
|
|
405
|
+
proc.stderr.on('data', (d) => { err += d.toString(); });
|
|
406
|
+
proc.on('close', (code) => {
|
|
407
|
+
if (code === 0) {
|
|
408
|
+
resolve(out);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
reject(new Error(err || 'Failed to get git diff'));
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
proc.on('error', reject);
|
|
415
|
+
});
|
|
416
|
+
if (!diff.trim()) {
|
|
417
|
+
return c.json({ error: 'No changes to generate PR info for. Make sure you have uncommitted changes.' }, 400);
|
|
418
|
+
}
|
|
419
|
+
// Truncate diff if too long (keep first 3000 chars)
|
|
420
|
+
const truncatedDiff = diff.length > 3000 ? diff.substring(0, 3000) + '\n...(truncated)' : diff;
|
|
421
|
+
const prompt = `Based on the following git diff, generate a pull request title and description. Follow these guidelines:
|
|
422
|
+
- Title: Concise, descriptive, under 72 characters
|
|
423
|
+
- Description: Clear explanation of changes, why they were made, and any relevant context
|
|
424
|
+
- Use markdown formatting for the description
|
|
425
|
+
- Be professional and clear
|
|
426
|
+
|
|
427
|
+
Git diff:
|
|
428
|
+
${truncatedDiff}
|
|
429
|
+
|
|
430
|
+
Generate the response in this exact format:
|
|
431
|
+
TITLE: <title here>
|
|
432
|
+
DESCRIPTION: <description here>`;
|
|
433
|
+
let session = null;
|
|
434
|
+
try {
|
|
435
|
+
const providerKeys = await metadataManager.getProviderKeys();
|
|
436
|
+
let selectedProvider = null;
|
|
437
|
+
let apiKey = null;
|
|
438
|
+
let selectedModel = model;
|
|
439
|
+
// If provider is specified, use it
|
|
440
|
+
if (provider) {
|
|
441
|
+
selectedProvider = PROVIDERS.find(p => p.name.toLowerCase() === provider.toLowerCase()) || null;
|
|
442
|
+
if (selectedProvider) {
|
|
443
|
+
// Try environment variable first
|
|
444
|
+
if (selectedProvider.keyName) {
|
|
445
|
+
apiKey = process.env[selectedProvider.keyName] || null;
|
|
446
|
+
}
|
|
447
|
+
// Fall back to configured key
|
|
448
|
+
if (!apiKey) {
|
|
449
|
+
apiKey = providerKeys[selectedProvider.name] || null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// If no provider specified or provider not found, use priority order
|
|
454
|
+
if (!selectedProvider || !apiKey) {
|
|
455
|
+
const providerPriority = ['Anthropic', 'OpenAI', 'OpenRouter', 'Groq', 'DeepSeek'];
|
|
456
|
+
// First try environment variables for priority providers
|
|
457
|
+
for (const providerName of providerPriority) {
|
|
458
|
+
const prov = PROVIDERS.find(p => p.name === providerName);
|
|
459
|
+
if (prov?.keyName) {
|
|
460
|
+
const envKey = process.env[prov.keyName];
|
|
461
|
+
if (envKey) {
|
|
462
|
+
selectedProvider = prov;
|
|
463
|
+
apiKey = envKey;
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// If no env key found, try configured keys
|
|
469
|
+
if (!selectedProvider || !apiKey) {
|
|
470
|
+
for (const providerName of providerPriority) {
|
|
471
|
+
const prov = PROVIDERS.find(p => p.name === providerName);
|
|
472
|
+
if (prov && providerKeys[prov.name]) {
|
|
473
|
+
selectedProvider = prov;
|
|
474
|
+
apiKey = providerKeys[prov.name];
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Fallback to any available provider
|
|
480
|
+
if (!selectedProvider || !apiKey) {
|
|
481
|
+
for (const prov of PROVIDERS) {
|
|
482
|
+
if (prov.api && prov.keyName) {
|
|
483
|
+
const envKey = process.env[prov.keyName];
|
|
484
|
+
if (envKey) {
|
|
485
|
+
selectedProvider = prov;
|
|
486
|
+
apiKey = envKey;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
if (providerKeys[prov.name]) {
|
|
490
|
+
selectedProvider = prov;
|
|
491
|
+
apiKey = providerKeys[prov.name];
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Select default model if not provided
|
|
498
|
+
if (!selectedModel) {
|
|
499
|
+
const modelMap = {
|
|
500
|
+
'Anthropic': 'claude-3-5-sonnet-20241022',
|
|
501
|
+
'OpenAI': 'gpt-4o-mini',
|
|
502
|
+
'OpenRouter': 'anthropic/claude-3.5-sonnet',
|
|
503
|
+
'Groq': 'llama-3.3-70b-versatile',
|
|
504
|
+
'DeepSeek': 'deepseek-chat',
|
|
505
|
+
};
|
|
506
|
+
selectedModel = modelMap[selectedProvider?.name || ''] || 'default';
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (!selectedProvider || !apiKey || !selectedProvider.api) {
|
|
510
|
+
throw new Error('No AI provider configured. Please configure an API key in settings.');
|
|
511
|
+
}
|
|
512
|
+
// Remove provider prefix from model if present
|
|
513
|
+
if (selectedModel && selectedProvider) {
|
|
514
|
+
selectedModel = selectedModel.replace(`${selectedProvider.name.toLowerCase()}/`, '');
|
|
515
|
+
}
|
|
516
|
+
const finalModel = selectedModel || 'default';
|
|
517
|
+
const providers = {
|
|
518
|
+
tarsk: {
|
|
519
|
+
api: selectedProvider.api,
|
|
520
|
+
options: { apiKey },
|
|
521
|
+
models: { [finalModel]: finalModel },
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
const sessionConfig = {
|
|
525
|
+
model: `tarsk/${finalModel}`,
|
|
526
|
+
cwd: process.cwd(),
|
|
527
|
+
productName: 'Tarsk.io',
|
|
528
|
+
providers,
|
|
529
|
+
};
|
|
530
|
+
session = await createSession(sessionConfig);
|
|
531
|
+
await session.send(prompt);
|
|
532
|
+
let prInfo = '';
|
|
533
|
+
for await (const msg of session.receive()) {
|
|
534
|
+
if (msg?.type === 'message' || msg?.type === 'result') {
|
|
535
|
+
const content = typeof msg.text === 'string' ? msg.text :
|
|
536
|
+
typeof msg.content === 'string' ? msg.content :
|
|
537
|
+
JSON.stringify(msg.content);
|
|
538
|
+
prInfo = content.trim();
|
|
539
|
+
if (msg?.type === 'result')
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Parse the response
|
|
544
|
+
const titleMatch = prInfo.match(/TITLE:\s*(.+?)(?:\nDESCRIPTION:|$)/s);
|
|
545
|
+
const descriptionMatch = prInfo.match(/DESCRIPTION:\s*(.+?)$/s);
|
|
546
|
+
const title = titleMatch ? titleMatch[1].trim() : 'Update';
|
|
547
|
+
const description = descriptionMatch ? descriptionMatch[1].trim() : '';
|
|
548
|
+
return c.json({ title, description });
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
if (session) {
|
|
552
|
+
try {
|
|
553
|
+
await session.close?.();
|
|
554
|
+
}
|
|
555
|
+
catch (closeError) {
|
|
556
|
+
console.error('Error closing session:', closeError);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
const message = error instanceof Error ? error.message : 'Failed to generate PR info';
|
|
563
|
+
return c.json({ error: message }, 500);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
// GET /api/git/log/:threadId - fetches git commit history
|
|
567
|
+
router.get('/log/:threadId', async (c) => {
|
|
568
|
+
try {
|
|
569
|
+
const threadId = c.req.param('threadId');
|
|
570
|
+
const limit = c.req.query('limit') ? parseInt(c.req.query('limit')) : 50;
|
|
571
|
+
const thread = await metadataManager.loadThreads().then(threads => threads.find(t => t.id === threadId));
|
|
572
|
+
if (!thread) {
|
|
573
|
+
return c.json({ error: 'Thread not found' }, 404);
|
|
574
|
+
}
|
|
575
|
+
const repoPath = thread.path;
|
|
576
|
+
if (!repoPath) {
|
|
577
|
+
return c.json({ error: 'Thread path not found' }, 404);
|
|
578
|
+
}
|
|
579
|
+
// Resolve absolute path - thread.path is already relative to process.cwd()
|
|
580
|
+
const absolutePath = resolve(repoPath);
|
|
581
|
+
// Get git log
|
|
582
|
+
const commits = await new Promise((resolve, reject) => {
|
|
583
|
+
const proc = spawn('git', ['log', '--oneline', '-n', limit.toString(), '--format=%H|%s|%an|%ai'], { cwd: absolutePath });
|
|
584
|
+
let out = '';
|
|
585
|
+
let err = '';
|
|
586
|
+
proc.stdout.on('data', (d) => { out += d.toString(); });
|
|
587
|
+
proc.stderr.on('data', (d) => { err += d.toString(); });
|
|
588
|
+
proc.on('close', (code) => {
|
|
589
|
+
if (code === 0) {
|
|
590
|
+
const lines = out.trim().split('\n').filter(line => line.length > 0);
|
|
591
|
+
const parsed = lines.map(line => {
|
|
592
|
+
const parts = line.split('|');
|
|
593
|
+
return {
|
|
594
|
+
hash: parts[0] || '',
|
|
595
|
+
message: parts[1] || '',
|
|
596
|
+
author: parts[2] || '',
|
|
597
|
+
date: parts[3] || ''
|
|
598
|
+
};
|
|
599
|
+
});
|
|
600
|
+
resolve(parsed);
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
reject(new Error(err || 'Failed to get git log'));
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
proc.on('error', reject);
|
|
607
|
+
});
|
|
608
|
+
return c.json({ commits });
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
const message = error instanceof Error ? error.message : 'Failed to fetch git log';
|
|
612
|
+
return c.json({ error: message }, 500);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
// POST /api/git/create-pr/:threadId - creates a pull request
|
|
616
|
+
router.post('/create-pr/:threadId', async (c) => {
|
|
617
|
+
try {
|
|
618
|
+
const threadId = c.req.param('threadId');
|
|
619
|
+
const body = await c.req.json();
|
|
620
|
+
const { title, description } = body;
|
|
621
|
+
const thread = await metadataManager.loadThreads().then(threads => threads.find(t => t.id === threadId));
|
|
622
|
+
if (!thread) {
|
|
623
|
+
return c.json({ error: 'Thread not found' }, 404);
|
|
624
|
+
}
|
|
625
|
+
const repoPath = thread.path;
|
|
626
|
+
if (!repoPath) {
|
|
627
|
+
return c.json({ error: 'Thread path not found' }, 404);
|
|
628
|
+
}
|
|
629
|
+
// Resolve absolute path - thread.path is already relative to process.cwd()
|
|
630
|
+
const absolutePath = resolve(repoPath);
|
|
631
|
+
// Get current branch
|
|
632
|
+
const currentBranch = await new Promise((resolve, reject) => {
|
|
633
|
+
const proc = spawn('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: absolutePath });
|
|
634
|
+
let out = '';
|
|
635
|
+
proc.stdout.on('data', (d) => { out += d.toString(); });
|
|
636
|
+
proc.on('close', () => resolve(out.trim()));
|
|
637
|
+
proc.on('error', reject);
|
|
638
|
+
});
|
|
639
|
+
// Try to create PR using gh CLI
|
|
640
|
+
const prUrl = await new Promise((resolve, reject) => {
|
|
641
|
+
const args = ['pr', 'create', '--title', title || currentBranch, '--body', description || ''];
|
|
642
|
+
const proc = spawn('gh', args, { cwd: absolutePath });
|
|
643
|
+
let out = '';
|
|
644
|
+
let err = '';
|
|
645
|
+
proc.stdout.on('data', (d) => { out += d.toString(); });
|
|
646
|
+
proc.stderr.on('data', (d) => { err += d.toString(); });
|
|
647
|
+
proc.on('close', (code) => {
|
|
648
|
+
if (code === 0) {
|
|
649
|
+
// Extract URL from output
|
|
650
|
+
const urlMatch = out.match(/https:\/\/[^\s]+/);
|
|
651
|
+
resolve(urlMatch ? urlMatch[0] : out.trim());
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
reject(new Error(err || 'Failed to create PR'));
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
proc.on('error', () => reject(new Error('GitHub CLI (gh) not found. Please install it to create PRs.')));
|
|
658
|
+
});
|
|
659
|
+
return c.json({ success: true, prUrl });
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
const message = error instanceof Error ? error.message : 'Failed to create PR';
|
|
663
|
+
return c.json({ error: message }, 500);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
return router;
|
|
667
|
+
}
|
|
668
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model routes for the REST API
|
|
3
|
+
*
|
|
4
|
+
* Handles all model-related operations:
|
|
5
|
+
* - GET /api/models - Get available models for a provider
|
|
6
|
+
* - POST /api/models/:provider/:modelId/enable - Enable a model
|
|
7
|
+
* - POST /api/models/:provider/:modelId/disable - Disable a model
|
|
8
|
+
* - GET /api/models/enabled - Get all enabled models
|
|
9
|
+
*/
|
|
10
|
+
import { Hono } from 'hono';
|
|
11
|
+
import { MetadataManager } from '../managers/metadata-manager.js';
|
|
12
|
+
/**
|
|
13
|
+
* Creates model routes
|
|
14
|
+
* @param metadataManager - Manager for metadata persistence
|
|
15
|
+
* @returns Hono router with model routes
|
|
16
|
+
*/
|
|
17
|
+
export declare function createModelRoutes(metadataManager: MetadataManager): Hono;
|
|
18
|
+
//# sourceMappingURL=models.d.ts.map
|