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.
Files changed (119) hide show
  1. package/README.md +1 -7
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +92 -32
  4. package/dist/lib/response-builder.d.ts +50 -0
  5. package/dist/lib/response-builder.js +56 -0
  6. package/dist/lib/stream-helper.d.ts +39 -0
  7. package/dist/lib/stream-helper.js +43 -0
  8. package/dist/managers/ConversationManager.d.ts +83 -0
  9. package/dist/managers/ConversationManager.js +129 -0
  10. package/dist/managers/GitManager.d.ts +133 -0
  11. package/dist/managers/GitManager.js +330 -0
  12. package/dist/managers/MetadataManager.d.ts +139 -0
  13. package/dist/managers/MetadataManager.js +309 -0
  14. package/dist/managers/ModelManager.d.ts +57 -0
  15. package/dist/managers/ModelManager.js +129 -0
  16. package/dist/managers/NeovateExecutor.d.ts +40 -0
  17. package/dist/managers/NeovateExecutor.js +138 -0
  18. package/dist/managers/ProjectManager.d.ts +162 -0
  19. package/dist/managers/ProjectManager.js +353 -0
  20. package/dist/managers/ThreadManager.d.ts +181 -0
  21. package/dist/managers/ThreadManager.js +325 -0
  22. package/dist/managers/conversation-manager.d.ts +83 -0
  23. package/dist/managers/conversation-manager.js +129 -0
  24. package/dist/managers/git-manager.d.ts +133 -0
  25. package/dist/managers/git-manager.js +330 -0
  26. package/dist/managers/metadata-manager.d.ts +139 -0
  27. package/dist/managers/metadata-manager.js +305 -0
  28. package/dist/managers/model-manager.d.ts +59 -0
  29. package/dist/managers/model-manager.js +144 -0
  30. package/dist/managers/neovate-executor.d.ts +43 -0
  31. package/dist/managers/neovate-executor.js +205 -0
  32. package/dist/managers/processing-state-manager.d.ts +40 -0
  33. package/dist/managers/processing-state-manager.js +27 -0
  34. package/dist/managers/project-manager.d.ts +199 -0
  35. package/dist/managers/project-manager.js +465 -0
  36. package/dist/managers/thread-manager.d.ts +193 -0
  37. package/dist/managers/thread-manager.js +368 -0
  38. package/dist/model-info-aihubmix.d.ts +25 -0
  39. package/dist/model-info-aihubmix.js +117 -0
  40. package/dist/model-info-openai.d.ts +17 -0
  41. package/dist/model-info-openai.js +59 -0
  42. package/dist/model-info-openrouter.d.ts +25 -0
  43. package/dist/model-info-openrouter.js +101 -0
  44. package/dist/model-info.d.ts +37 -0
  45. package/dist/model-info.js +39 -0
  46. package/dist/provider-data.d.ts +101 -0
  47. package/dist/provider-data.js +471 -0
  48. package/dist/provider.d.ts +10 -0
  49. package/dist/provider.js +192 -0
  50. package/dist/public/android-chrome-192x192.png +0 -0
  51. package/dist/public/android-chrome-512x512.png +0 -0
  52. package/dist/public/apple-touch-icon.png +0 -0
  53. package/dist/public/assets/index-B443aj9k.js +8506 -0
  54. package/dist/public/assets/index-CjXGVbI7.css +1 -0
  55. package/dist/public/assets/index-DJC-p914.js +8506 -0
  56. package/dist/public/favicon-16x16.png +0 -0
  57. package/dist/public/favicon-32x32.png +0 -0
  58. package/dist/public/favicon.ico +0 -0
  59. package/dist/public/index.html +28 -0
  60. package/dist/public/manifest.json +82 -0
  61. package/dist/public/placeholder-logo.svg +1 -0
  62. package/dist/public/placeholder.svg +1 -0
  63. package/dist/public/snpro.woff2 +0 -0
  64. package/dist/public/tarsk-color.svg +12 -0
  65. package/dist/public/tarsk.png +0 -0
  66. package/dist/public/tarsk.svg +12 -0
  67. package/dist/public/zalando-sans.woff2 +0 -0
  68. package/dist/routes/chat-old.d.ts +21 -0
  69. package/dist/routes/chat-old.js +251 -0
  70. package/dist/routes/chat.d.ts +21 -0
  71. package/dist/routes/chat.js +217 -0
  72. package/dist/routes/git.d.ts +4 -0
  73. package/dist/routes/git.js +668 -0
  74. package/dist/routes/models.d.ts +18 -0
  75. package/dist/routes/models.js +128 -0
  76. package/dist/routes/projects-old.d.ts +20 -0
  77. package/dist/routes/projects-old.js +297 -0
  78. package/dist/routes/projects.d.ts +20 -0
  79. package/dist/routes/projects.js +365 -0
  80. package/dist/routes/providers.d.ts +15 -0
  81. package/dist/routes/providers.js +130 -0
  82. package/dist/routes/threads-old.d.ts +14 -0
  83. package/dist/routes/threads-old.js +393 -0
  84. package/dist/routes/threads.d.ts +14 -0
  85. package/dist/routes/threads.js +352 -0
  86. package/dist/types/models.d.ts +315 -0
  87. package/dist/types/models.js +11 -0
  88. package/dist/utils/env-manager.d.ts +3 -0
  89. package/dist/utils/env-manager.js +60 -0
  90. package/dist/utils/open-router-models.d.ts +45 -0
  91. package/dist/utils/open-router-models.js +103 -0
  92. package/dist/utils/openai-models.d.ts +63 -0
  93. package/dist/utils/openai-models.js +152 -0
  94. package/dist/utils/openai-pricing-scraper.d.ts +17 -0
  95. package/dist/utils/openai-pricing-scraper.js +185 -0
  96. package/dist/utils/validation.d.ts +10 -0
  97. package/dist/utils/validation.js +20 -0
  98. package/dist/utils.d.ts +10 -0
  99. package/dist/utils.js +12 -0
  100. package/package.json +36 -22
  101. package/LICENSE.md +0 -7
  102. package/dist/agent/agent.js +0 -131
  103. package/dist/agent/interfaces.js +0 -1
  104. package/dist/api/encryption.js +0 -41
  105. package/dist/api/models.js +0 -169
  106. package/dist/api/prompt.js +0 -12
  107. package/dist/api/settings.js +0 -43
  108. package/dist/api/test.js +0 -29
  109. package/dist/api/tools.js +0 -287
  110. package/dist/api/utils.js +0 -18
  111. package/dist/interfaces/meta.js +0 -1
  112. package/dist/interfaces/model.js +0 -1
  113. package/dist/interfaces/settings.js +0 -1
  114. package/dist/log/log.js +0 -33
  115. package/dist/prompt.js +0 -49
  116. package/dist/tools.js +0 -84
  117. package/dist/utils/files.js +0 -14
  118. package/dist/utils/json-file.js +0 -28
  119. 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