tarsk 0.3.19 → 0.3.20
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/dist/index.js +24 -8
- package/dist/lib/conversation-content.d.ts +28 -0
- package/dist/lib/conversation-content.js +130 -0
- package/dist/lib/response-builder.d.ts +1 -1
- package/dist/managers/conversation-manager.d.ts +1 -4
- package/dist/managers/git-manager.d.ts +7 -1
- package/dist/managers/git-manager.js +8 -1
- package/dist/managers/metadata-manager.d.ts +14 -12
- package/dist/managers/metadata-manager.js +65 -6
- package/dist/managers/model-manager.d.ts +1 -1
- package/dist/managers/neovate-executor.d.ts +1 -1
- package/dist/managers/process-manager.d.ts +55 -0
- package/dist/managers/process-manager.js +221 -0
- package/dist/managers/project-manager.d.ts +110 -9
- package/dist/managers/project-manager.js +327 -82
- package/dist/managers/thread-manager.d.ts +1 -7
- package/dist/provider-data.d.ts +9 -9
- package/dist/provider-data.js +16 -5
- package/dist/public/assets/index-BLGMN24v.css +1 -0
- package/dist/public/assets/index-ClP26kjT.js +86 -0
- package/dist/public/index.html +2 -2
- package/dist/public/template-types/logo-angular.svg +33 -0
- package/dist/public/template-types/logo-astro.svg +11 -0
- package/dist/public/template-types/logo-capacitor.svg +8 -0
- package/dist/public/template-types/logo-hydrogen.svg +1 -0
- package/dist/public/template-types/logo-lit.svg +1 -0
- package/dist/public/template-types/logo-nextjs.svg +11 -0
- package/dist/public/template-types/logo-nuxt.svg +3 -0
- package/dist/public/template-types/logo-preact.svg +6 -0
- package/dist/public/template-types/logo-qwik.svg +5 -0
- package/dist/public/template-types/logo-react.svg +1 -0
- package/dist/public/template-types/logo-solid.svg +1 -0
- package/dist/public/template-types/logo-svelte.svg +15 -0
- package/dist/public/template-types/logo-vite.svg +15 -0
- package/dist/public/template-types/logo-vue.svg +1 -0
- package/dist/public/template-types/logo-waku.svg +19 -0
- package/dist/public/template-types/logo-web.svg +14 -0
- package/dist/routes/chat.js +21 -10
- package/dist/routes/git.js +173 -0
- package/dist/routes/onboarding.d.ts +17 -0
- package/dist/routes/onboarding.js +94 -0
- package/dist/routes/projects.js +158 -65
- package/dist/routes/run.d.ts +18 -0
- package/dist/routes/run.js +180 -0
- package/dist/routes/scaffold.d.ts +10 -0
- package/dist/routes/scaffold.js +48 -0
- package/dist/routes/threads.js +44 -45
- package/dist/scaffold/index.d.ts +7 -0
- package/dist/scaffold/index.js +5 -0
- package/dist/scaffold/runner.d.ts +46 -0
- package/dist/scaffold/runner.js +378 -0
- package/dist/scaffold/types.d.ts +24 -0
- package/dist/scaffold/types.js +5 -0
- package/dist/scaffold-templates.json +559 -0
- package/dist/utils/crypto.d.ts +29 -0
- package/dist/utils/crypto.js +122 -0
- package/dist/utils/open-router-models.d.ts +1 -1
- package/dist/utils/openai-models.d.ts +1 -1
- package/dist/utils/run-command-detector.d.ts +26 -0
- package/dist/utils/run-command-detector.js +98 -0
- package/package.json +7 -4
- package/dist/public/assets/index-DBN5QJqj.css +0 -1
- package/dist/public/assets/index-DsG11Rhy.js +0 -8499
- package/dist/public/placeholder-logo.svg +0 -1
- package/dist/public/placeholder.svg +0 -1
- package/dist/types/models.d.ts +0 -325
- package/dist/types/models.js +0 -11
package/dist/index.js
CHANGED
|
@@ -19,7 +19,10 @@ import { createChatRoutes } from './routes/chat.js';
|
|
|
19
19
|
import { createProviderRoutes } from './routes/providers.js';
|
|
20
20
|
import { createModelRoutes } from './routes/models.js';
|
|
21
21
|
import { createGitRoutes } from './routes/git.js';
|
|
22
|
-
import {
|
|
22
|
+
import { createRunRoutes } from './routes/run.js';
|
|
23
|
+
import { createOnboardingRoutes } from './routes/onboarding.js';
|
|
24
|
+
import { createScaffoldRoutes } from './routes/scaffold.js';
|
|
25
|
+
import { AVAILABLE_PROGRAMS } from '@tarsk/shared';
|
|
23
26
|
import { getDataDir } from './paths.js';
|
|
24
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
28
|
const __dirname = path.dirname(__filename);
|
|
@@ -67,21 +70,34 @@ app.get('/api/threads/processing', (c) => {
|
|
|
67
70
|
});
|
|
68
71
|
// Register API routes
|
|
69
72
|
app.route('/api/projects', createProjectRoutes(projectManager, threadManager));
|
|
73
|
+
app.route('/api/projects', createRunRoutes(projectManager));
|
|
70
74
|
app.route('/api/threads', createThreadRoutes(threadManager, gitManager, conversationManager));
|
|
71
75
|
app.route('/api/chat', createChatRoutes(threadManager, neovateExecutor, conversationManager, processingStateManager));
|
|
72
76
|
app.route('/api/providers', createProviderRoutes(metadataManager));
|
|
73
77
|
app.route('/api/models', createModelRoutes(metadataManager));
|
|
74
78
|
app.route('/api/git', createGitRoutes(metadataManager));
|
|
79
|
+
app.route('/api/onboarding', createOnboardingRoutes(metadataManager));
|
|
80
|
+
app.route('/api/scaffold', createScaffoldRoutes(projectManager));
|
|
75
81
|
// Serve static files from the 'public' directory
|
|
76
82
|
// In production, this will be the built frontend app
|
|
77
83
|
const publicDir = path.join(__dirname, 'public');
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}));
|
|
84
|
+
const staticRoot = path.relative(process.cwd(), publicDir);
|
|
85
|
+
app.use('/*', async (c, next) => {
|
|
86
|
+
// Never serve static files or SPA fallback for API routes
|
|
87
|
+
if (c.req.path.startsWith('/api/')) {
|
|
88
|
+
return next();
|
|
89
|
+
}
|
|
90
|
+
return serveStatic({ root: staticRoot })(c, next);
|
|
91
|
+
});
|
|
92
|
+
// Fallback to index.html for SPA routing (only for non-API paths)
|
|
93
|
+
app.get('*', async (c, next) => {
|
|
94
|
+
if (c.req.path.startsWith('/api/')) {
|
|
95
|
+
return next();
|
|
96
|
+
}
|
|
97
|
+
return serveStatic({
|
|
98
|
+
path: path.relative(process.cwd(), path.join(publicDir, 'index.html'))
|
|
99
|
+
})(c, next);
|
|
100
|
+
});
|
|
85
101
|
// Error handling middleware for unmatched routes
|
|
86
102
|
app.all('*', (c) => {
|
|
87
103
|
const errorResponse = {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for extracting and normalizing conversation content from Neovate events.
|
|
3
|
+
* Used when saving completed messages (store only final answer) and when
|
|
4
|
+
* mapping stream events to thinking vs content for the UI.
|
|
5
|
+
*/
|
|
6
|
+
import type { NeovateEvent, ConversationMessage } from '@tarsk/shared';
|
|
7
|
+
/**
|
|
8
|
+
* Formats completed conversation messages into a single context string to prepend
|
|
9
|
+
* to the next user prompt so the model retains conversation history.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatConversationContext(messages: ConversationMessage[], maxMessages?: number): string;
|
|
12
|
+
/**
|
|
13
|
+
* Parses assistant message content into content blocks when it is a JSON array
|
|
14
|
+
* (e.g. [{ type: 'reasoning', text: '...' }, { type: 'text', text: '...' }, { type: 'tool_use', ... }]).
|
|
15
|
+
* Returns null if content is not a structured array.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseAssistantContentBlocks(content: string): Array<{
|
|
18
|
+
type: string;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}> | null;
|
|
21
|
+
/** Returns true if content looks like tool result/use JSON */
|
|
22
|
+
export declare function isToolLikeContent(content: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Returns only the final assistant answer text from events (no user message, no tool JSON).
|
|
25
|
+
* Use this when persisting response content so the stored file stays clean.
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractAssistantContent(events: NeovateEvent[] | undefined, fallback: string): string;
|
|
28
|
+
//# sourceMappingURL=conversation-content.d.ts.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for extracting and normalizing conversation content from Neovate events.
|
|
3
|
+
* Used when saving completed messages (store only final answer) and when
|
|
4
|
+
* mapping stream events to thinking vs content for the UI.
|
|
5
|
+
*/
|
|
6
|
+
/** Max number of prior exchanges to include in context (user + assistant pairs) */
|
|
7
|
+
const DEFAULT_MAX_CONTEXT_MESSAGES = 10;
|
|
8
|
+
/**
|
|
9
|
+
* Formats completed conversation messages into a single context string to prepend
|
|
10
|
+
* to the next user prompt so the model retains conversation history.
|
|
11
|
+
*/
|
|
12
|
+
export function formatConversationContext(messages, maxMessages = DEFAULT_MAX_CONTEXT_MESSAGES) {
|
|
13
|
+
const completed = messages.filter((m) => m.response != null);
|
|
14
|
+
const recent = completed.slice(-maxMessages);
|
|
15
|
+
if (recent.length === 0)
|
|
16
|
+
return '';
|
|
17
|
+
const lines = ['Previous conversation:'];
|
|
18
|
+
for (const m of recent) {
|
|
19
|
+
lines.push(`User: ${m.request.message}`);
|
|
20
|
+
lines.push(`Assistant: ${m.response.content}`);
|
|
21
|
+
}
|
|
22
|
+
return lines.join('\n');
|
|
23
|
+
}
|
|
24
|
+
const extractTextBlocksFromContent = (content) => {
|
|
25
|
+
const trimmed = content.trim();
|
|
26
|
+
if (!trimmed.startsWith('[') && !trimmed.startsWith('{')) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(trimmed);
|
|
31
|
+
if (Array.isArray(parsed)) {
|
|
32
|
+
return parsed
|
|
33
|
+
.filter((block) => block && typeof block === 'object' && block.type === 'text')
|
|
34
|
+
.map((block) => (typeof block.text === 'string' ? block.text : ''))
|
|
35
|
+
.filter((text) => text.length > 0);
|
|
36
|
+
}
|
|
37
|
+
if (parsed && typeof parsed === 'object' && 'text' in parsed && typeof parsed.text === 'string') {
|
|
38
|
+
return [parsed.text];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// ignore
|
|
43
|
+
}
|
|
44
|
+
return [];
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Parses assistant message content into content blocks when it is a JSON array
|
|
48
|
+
* (e.g. [{ type: 'reasoning', text: '...' }, { type: 'text', text: '...' }, { type: 'tool_use', ... }]).
|
|
49
|
+
* Returns null if content is not a structured array.
|
|
50
|
+
*/
|
|
51
|
+
export function parseAssistantContentBlocks(content) {
|
|
52
|
+
const trimmed = content.trim();
|
|
53
|
+
if (!trimmed.startsWith('['))
|
|
54
|
+
return null;
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(trimmed);
|
|
57
|
+
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
58
|
+
return null;
|
|
59
|
+
const blocks = [];
|
|
60
|
+
for (const item of parsed) {
|
|
61
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
62
|
+
blocks.push(item);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return blocks.length > 0 ? blocks : null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Returns true if content looks like tool result/use JSON */
|
|
72
|
+
export function isToolLikeContent(content) {
|
|
73
|
+
const trimmed = content.trim();
|
|
74
|
+
if (!trimmed.startsWith('[') && !trimmed.startsWith('{'))
|
|
75
|
+
return false;
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(trimmed);
|
|
78
|
+
if (Array.isArray(parsed)) {
|
|
79
|
+
return parsed.some((item) => item &&
|
|
80
|
+
typeof item === 'object' &&
|
|
81
|
+
'type' in item &&
|
|
82
|
+
(item.type === 'tool-result' || item.type === 'tool_use'));
|
|
83
|
+
}
|
|
84
|
+
return (typeof parsed === 'object' &&
|
|
85
|
+
parsed !== null &&
|
|
86
|
+
'type' in parsed &&
|
|
87
|
+
(parsed.type === 'tool-result' || parsed.type === 'tool_use'));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Returns only the final assistant answer text from events (no user message, no tool JSON).
|
|
95
|
+
* Use this when persisting response content so the stored file stays clean.
|
|
96
|
+
*/
|
|
97
|
+
export function extractAssistantContent(events, fallback) {
|
|
98
|
+
if (!events || events.length === 0) {
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
101
|
+
const resultEvent = [...events].reverse().find((event) => event.type === 'result');
|
|
102
|
+
if (resultEvent && typeof resultEvent.content === 'string' && resultEvent.content.trim().length > 0) {
|
|
103
|
+
return resultEvent.content;
|
|
104
|
+
}
|
|
105
|
+
const assistantMessages = events.filter((event) => event.type === 'message' && event.role === 'assistant');
|
|
106
|
+
let lastTextBlock = null;
|
|
107
|
+
const assistantChunks = [];
|
|
108
|
+
for (const event of assistantMessages) {
|
|
109
|
+
if (typeof event.content !== 'string') {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (isToolLikeContent(event.content)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
assistantChunks.push(event.content);
|
|
116
|
+
const textBlocks = extractTextBlocksFromContent(event.content);
|
|
117
|
+
if (textBlocks.length > 0) {
|
|
118
|
+
lastTextBlock = textBlocks[textBlocks.length - 1];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (lastTextBlock) {
|
|
122
|
+
return lastTextBlock;
|
|
123
|
+
}
|
|
124
|
+
if (assistantChunks.length === 0) {
|
|
125
|
+
return fallback;
|
|
126
|
+
}
|
|
127
|
+
const combined = assistantChunks.join('');
|
|
128
|
+
return combined.trim().length > 0 ? combined : fallback;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=conversation-content.js.map
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* across all route handlers. This eliminates duplication of response formatting
|
|
6
6
|
* logic and ensures consistent error handling throughout the API.
|
|
7
7
|
*/
|
|
8
|
-
import type { ErrorResponse } from '
|
|
8
|
+
import type { ErrorResponse } from '@tarsk/shared';
|
|
9
9
|
/**
|
|
10
10
|
* Factory for building consistent API responses
|
|
11
11
|
*/
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Retrieving conversation history for a thread
|
|
7
7
|
* - Managing conversation persistence
|
|
8
8
|
*/
|
|
9
|
-
import { ConversationHistory, ConversationMessage, NeovateEvent, FileData } from '
|
|
9
|
+
import { ConversationHistory, ConversationMessage, NeovateEvent, FileData } from '@tarsk/shared';
|
|
10
10
|
/**
|
|
11
11
|
* ConversationManager interface defines the contract for conversation operations
|
|
12
12
|
*/
|
|
@@ -71,9 +71,6 @@ export declare class ConversationManagerImpl implements ConversationManager {
|
|
|
71
71
|
* Gets the last N messages from conversation history
|
|
72
72
|
*/
|
|
73
73
|
getLastMessages(threadPath: string, count?: number): Promise<ConversationMessage[]>;
|
|
74
|
-
/**
|
|
75
|
-
* Saves conversation history to file
|
|
76
|
-
*/
|
|
77
74
|
private saveConversationHistory;
|
|
78
75
|
/**
|
|
79
76
|
* Gets the file path for a thread's conversation history in the metadata folder
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Cloning repositories with streaming output
|
|
7
7
|
* - Handling git operation errors
|
|
8
8
|
*/
|
|
9
|
-
import { GitEvent } from '
|
|
9
|
+
import { GitEvent } from '@tarsk/shared';
|
|
10
10
|
/**
|
|
11
11
|
* GitManager interface defines the contract for git operations
|
|
12
12
|
*/
|
|
@@ -45,6 +45,11 @@ export interface GitManager {
|
|
|
45
45
|
* @yields GitEvent objects for stdout, stderr, complete, and error events
|
|
46
46
|
*/
|
|
47
47
|
createAndCheckoutBranch(repoPath: string, branchName: string): AsyncGenerator<GitEvent>;
|
|
48
|
+
/**
|
|
49
|
+
* Initializes a new git repository at the given path
|
|
50
|
+
* @param repoPath - The path where to run git init
|
|
51
|
+
*/
|
|
52
|
+
initRepository(repoPath: string): Promise<void>;
|
|
48
53
|
}
|
|
49
54
|
/**
|
|
50
55
|
* GitManagerImpl provides the implementation for git operations
|
|
@@ -129,5 +134,6 @@ export declare class GitManagerImpl implements GitManager {
|
|
|
129
134
|
* @yields GitEvent objects during the branch creation
|
|
130
135
|
*/
|
|
131
136
|
createAndCheckoutBranch(repoPath: string, branchName: string): AsyncGenerator<GitEvent>;
|
|
137
|
+
initRepository(repoPath: string): Promise<void>;
|
|
132
138
|
}
|
|
133
139
|
//# sourceMappingURL=git-manager.d.ts.map
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Cloning repositories with streaming output
|
|
7
7
|
* - Handling git operation errors
|
|
8
8
|
*/
|
|
9
|
-
import { spawn } from 'child_process';
|
|
9
|
+
import { spawn, spawnSync } from 'child_process';
|
|
10
10
|
import { mkdir } from 'fs/promises';
|
|
11
11
|
import { dirname } from 'path';
|
|
12
12
|
/**
|
|
@@ -326,5 +326,12 @@ export class GitManagerImpl {
|
|
|
326
326
|
};
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
|
+
async initRepository(repoPath) {
|
|
330
|
+
const result = spawnSync('git', ['init'], { cwd: repoPath, encoding: 'utf-8' });
|
|
331
|
+
if (result.status !== 0) {
|
|
332
|
+
const stderr = result.stderr?.trim() || result.error?.message || 'Unknown error';
|
|
333
|
+
throw new Error(`git init failed: ${stderr}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
329
336
|
}
|
|
330
337
|
//# sourceMappingURL=git-manager.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This manager provides JSON file-based storage with atomic writes
|
|
5
5
|
* to ensure data consistency even if the process crashes during write operations.
|
|
6
6
|
*/
|
|
7
|
-
import { Project, Thread } from "
|
|
7
|
+
import { Project, Thread } from "@tarsk/shared";
|
|
8
8
|
/**
|
|
9
9
|
* MetadataManager manages persistence of projects and threads
|
|
10
10
|
*/
|
|
@@ -12,6 +12,7 @@ interface AppState {
|
|
|
12
12
|
selectedThreadId: string | null;
|
|
13
13
|
providerKeys: Record<string, string>;
|
|
14
14
|
enabledModels: Record<string, string[]>;
|
|
15
|
+
onboardingCompleted: boolean;
|
|
15
16
|
}
|
|
16
17
|
export declare class MetadataManager {
|
|
17
18
|
private projectsFile;
|
|
@@ -48,15 +49,6 @@ export declare class MetadataManager {
|
|
|
48
49
|
* @returns Array of threads
|
|
49
50
|
*/
|
|
50
51
|
loadThreads(): Promise<Thread[]>;
|
|
51
|
-
/**
|
|
52
|
-
* Perform atomic write using temp file
|
|
53
|
-
*
|
|
54
|
-
* This ensures that if the process crashes during write,
|
|
55
|
-
* we don't end up with corrupted or partial data.
|
|
56
|
-
*
|
|
57
|
-
* @param filePath - Target file path
|
|
58
|
-
* @param data - Data to write
|
|
59
|
-
*/
|
|
60
52
|
private atomicWrite;
|
|
61
53
|
/**
|
|
62
54
|
* Get the metadata directory path
|
|
@@ -88,12 +80,12 @@ export declare class MetadataManager {
|
|
|
88
80
|
/**
|
|
89
81
|
* Update provider keys
|
|
90
82
|
* @param providerName - Name of the provider
|
|
91
|
-
* @param apiKey - API key to save
|
|
83
|
+
* @param apiKey - API key to save (will be encrypted)
|
|
92
84
|
*/
|
|
93
85
|
saveProviderKey(providerName: string, apiKey: string): Promise<void>;
|
|
94
86
|
/**
|
|
95
87
|
* Get all provider keys
|
|
96
|
-
* @returns Record of provider names to API keys
|
|
88
|
+
* @returns Record of provider names to decrypted API keys
|
|
97
89
|
*/
|
|
98
90
|
getProviderKeys(): Promise<Record<string, string>>;
|
|
99
91
|
/**
|
|
@@ -135,6 +127,16 @@ export declare class MetadataManager {
|
|
|
135
127
|
* @param modelIds - Array of model IDs to enable
|
|
136
128
|
*/
|
|
137
129
|
setEnabledModels(provider: string, modelIds: string[]): Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* Set onboarding completed flag
|
|
132
|
+
* @param completed - Whether onboarding is completed
|
|
133
|
+
*/
|
|
134
|
+
setOnboardingCompleted(completed: boolean): Promise<void>;
|
|
135
|
+
/**
|
|
136
|
+
* Get onboarding completed flag
|
|
137
|
+
* @returns Whether onboarding has been completed
|
|
138
|
+
*/
|
|
139
|
+
getOnboardingCompleted(): Promise<boolean>;
|
|
138
140
|
}
|
|
139
141
|
export {};
|
|
140
142
|
//# sourceMappingURL=metadata-manager.d.ts.map
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { promises as fs } from "fs";
|
|
8
8
|
import { join, dirname } from "path";
|
|
9
|
+
import { encrypt, decrypt, isEncrypted } from "../utils/crypto.js";
|
|
9
10
|
export class MetadataManager {
|
|
10
11
|
projectsFile;
|
|
11
12
|
threadsFile;
|
|
@@ -49,6 +50,7 @@ export class MetadataManager {
|
|
|
49
50
|
selectedThreadId: null,
|
|
50
51
|
providerKeys: {},
|
|
51
52
|
enabledModels: {},
|
|
53
|
+
onboardingCompleted: false,
|
|
52
54
|
});
|
|
53
55
|
}
|
|
54
56
|
}
|
|
@@ -187,11 +189,15 @@ export class MetadataManager {
|
|
|
187
189
|
if (!state.enabledModels) {
|
|
188
190
|
state.enabledModels = {};
|
|
189
191
|
}
|
|
192
|
+
// Ensure onboardingCompleted exists
|
|
193
|
+
if (state.onboardingCompleted === undefined) {
|
|
194
|
+
state.onboardingCompleted = false;
|
|
195
|
+
}
|
|
190
196
|
return state;
|
|
191
197
|
}
|
|
192
198
|
catch (error) {
|
|
193
199
|
if (error.code === "ENOENT") {
|
|
194
|
-
return { selectedThreadId: null, providerKeys: {}, enabledModels: {} };
|
|
200
|
+
return { selectedThreadId: null, providerKeys: {}, enabledModels: {}, onboardingCompleted: false };
|
|
195
201
|
}
|
|
196
202
|
throw new Error(`Failed to load state: ${error}`);
|
|
197
203
|
}
|
|
@@ -202,26 +208,62 @@ export class MetadataManager {
|
|
|
202
208
|
*/
|
|
203
209
|
async saveAllProviderKeys(keys) {
|
|
204
210
|
const state = await this.loadState();
|
|
205
|
-
|
|
211
|
+
// Encrypt all keys before saving
|
|
212
|
+
const encryptedKeys = {};
|
|
213
|
+
for (const [provider, key] of Object.entries(keys)) {
|
|
214
|
+
encryptedKeys[provider] = key ? encrypt(key) : '';
|
|
215
|
+
}
|
|
216
|
+
state.providerKeys = { ...state.providerKeys, ...encryptedKeys };
|
|
206
217
|
await this.saveState(state);
|
|
207
218
|
}
|
|
208
219
|
/**
|
|
209
220
|
* Update provider keys
|
|
210
221
|
* @param providerName - Name of the provider
|
|
211
|
-
* @param apiKey - API key to save
|
|
222
|
+
* @param apiKey - API key to save (will be encrypted)
|
|
212
223
|
*/
|
|
213
224
|
async saveProviderKey(providerName, apiKey) {
|
|
214
225
|
const state = await this.loadState();
|
|
215
|
-
|
|
226
|
+
// Encrypt the API key before saving
|
|
227
|
+
state.providerKeys[providerName] = apiKey ? encrypt(apiKey) : '';
|
|
216
228
|
await this.saveState(state);
|
|
217
229
|
}
|
|
218
230
|
/**
|
|
219
231
|
* Get all provider keys
|
|
220
|
-
* @returns Record of provider names to API keys
|
|
232
|
+
* @returns Record of provider names to decrypted API keys
|
|
221
233
|
*/
|
|
222
234
|
async getProviderKeys() {
|
|
223
235
|
const state = await this.loadState();
|
|
224
|
-
|
|
236
|
+
// Decrypt all keys and migrate any plain-text keys
|
|
237
|
+
const decryptedKeys = {};
|
|
238
|
+
let needsMigration = false;
|
|
239
|
+
for (const [provider, encryptedKey] of Object.entries(state.providerKeys)) {
|
|
240
|
+
if (!encryptedKey) {
|
|
241
|
+
decryptedKeys[provider] = '';
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
// Check if key is already encrypted
|
|
245
|
+
if (isEncrypted(encryptedKey)) {
|
|
246
|
+
try {
|
|
247
|
+
decryptedKeys[provider] = decrypt(encryptedKey);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
console.error(`Failed to decrypt key for ${provider}:`, error);
|
|
251
|
+
// Keep the encrypted value if decryption fails
|
|
252
|
+
decryptedKeys[provider] = '';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// Plain-text key found - needs migration
|
|
257
|
+
decryptedKeys[provider] = encryptedKey;
|
|
258
|
+
needsMigration = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// If we found plain-text keys, encrypt them
|
|
262
|
+
if (needsMigration) {
|
|
263
|
+
console.log('Migrating plain-text API keys to encrypted format...');
|
|
264
|
+
await this.saveAllProviderKeys(decryptedKeys);
|
|
265
|
+
}
|
|
266
|
+
return decryptedKeys;
|
|
225
267
|
}
|
|
226
268
|
/**
|
|
227
269
|
* Set the selected thread ID
|
|
@@ -302,5 +344,22 @@ export class MetadataManager {
|
|
|
302
344
|
}
|
|
303
345
|
await this.saveState(state);
|
|
304
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Set onboarding completed flag
|
|
349
|
+
* @param completed - Whether onboarding is completed
|
|
350
|
+
*/
|
|
351
|
+
async setOnboardingCompleted(completed) {
|
|
352
|
+
const state = await this.loadState();
|
|
353
|
+
state.onboardingCompleted = completed;
|
|
354
|
+
await this.saveState(state);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Get onboarding completed flag
|
|
358
|
+
* @returns Whether onboarding has been completed
|
|
359
|
+
*/
|
|
360
|
+
async getOnboardingCompleted() {
|
|
361
|
+
const state = await this.loadState();
|
|
362
|
+
return state.onboardingCompleted;
|
|
363
|
+
}
|
|
305
364
|
}
|
|
306
365
|
//# sourceMappingURL=metadata-manager.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Manages available and enabled models across different providers.
|
|
5
5
|
* Enriches models with pricing (and name/description) from model-info when available (e.g. OpenRouter).
|
|
6
6
|
*/
|
|
7
|
-
import { Model } from '
|
|
7
|
+
import { Model } from '@tarsk/shared';
|
|
8
8
|
import { MetadataManager } from './metadata-manager.js';
|
|
9
9
|
/**
|
|
10
10
|
* ModelManager handles model availability and selection
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Streaming result messages back to the caller
|
|
7
7
|
* - Handling execution errors
|
|
8
8
|
*/
|
|
9
|
-
import { NeovateEvent, ExecutionContext } from "
|
|
9
|
+
import { NeovateEvent, ExecutionContext } from "@tarsk/shared";
|
|
10
10
|
import { MetadataManager } from "./metadata-manager.js";
|
|
11
11
|
/**
|
|
12
12
|
* NeovateExecutor interface defines the contract for neovate command execution
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProcessManager handles running and stopping dev server processes
|
|
3
|
+
*
|
|
4
|
+
* This manager is responsible for:
|
|
5
|
+
* - Starting dev server processes with output capture
|
|
6
|
+
* - Detecting URLs from process output
|
|
7
|
+
* - Stopping running processes
|
|
8
|
+
* - Streaming output back to the frontend
|
|
9
|
+
*/
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
export interface ProcessOutput {
|
|
12
|
+
type: 'stdout' | 'stderr' | 'url' | 'error' | 'complete';
|
|
13
|
+
content?: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
error?: unknown;
|
|
16
|
+
}
|
|
17
|
+
export declare class ProcessManager extends EventEmitter {
|
|
18
|
+
private processes;
|
|
19
|
+
private detectedUrls;
|
|
20
|
+
private queues;
|
|
21
|
+
private resolvers;
|
|
22
|
+
/**
|
|
23
|
+
* Run a command and stream its output
|
|
24
|
+
* @param projectId - The project ID (used as a key)
|
|
25
|
+
* @param command - The command to run (e.g., "npm run dev")
|
|
26
|
+
* @param cwd - Working directory to run the command in
|
|
27
|
+
* @returns AsyncGenerator that yields ProcessOutput events
|
|
28
|
+
*/
|
|
29
|
+
runProcess(projectId: string, command: string, cwd: string): AsyncGenerator<ProcessOutput>;
|
|
30
|
+
/**
|
|
31
|
+
* Stop a running process
|
|
32
|
+
* @param projectId - The project ID
|
|
33
|
+
* @returns true if process was stopped, false if no process was running
|
|
34
|
+
*/
|
|
35
|
+
stopProcess(projectId: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Check if a process is running for a project
|
|
38
|
+
* @param projectId - The project ID
|
|
39
|
+
* @returns true if a process is running
|
|
40
|
+
*/
|
|
41
|
+
isRunning(projectId: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Get the detected URL for a running process
|
|
44
|
+
* @param projectId - The project ID
|
|
45
|
+
* @returns The URL if detected, undefined otherwise
|
|
46
|
+
*/
|
|
47
|
+
getDetectedUrl(projectId: string): string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Detect URL from text output
|
|
50
|
+
* @param text - The text to search for URLs
|
|
51
|
+
* @returns The first detected URL or undefined
|
|
52
|
+
*/
|
|
53
|
+
private detectUrl;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=process-manager.d.ts.map
|