tarsk 0.3.17 → 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 +31 -16
- 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 +4 -6
- package/dist/managers/conversation-manager.js +4 -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 +17 -14
- package/dist/managers/metadata-manager.js +69 -9
- 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 +129 -6
- package/dist/managers/project-manager.js +410 -96
- package/dist/managers/thread-manager.d.ts +9 -7
- package/dist/managers/thread-manager.js +97 -28
- package/dist/paths.d.ts +22 -0
- package/dist/paths.js +26 -0
- package/dist/provider-data.d.ts +13 -13
- package/dist/provider-data.js +26 -9
- 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 +162 -23
- 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-CLr9LKtA.js +0 -8503
- package/dist/public/assets/index-CjXGVbI7.css +0 -1
- package/dist/public/placeholder-logo.svg +0 -1
- package/dist/public/placeholder.svg +0 -1
- package/dist/types/models.d.ts +0 -315
- package/dist/types/models.js +0 -11
package/dist/index.js
CHANGED
|
@@ -19,7 +19,11 @@ 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';
|
|
26
|
+
import { getDataDir } from './paths.js';
|
|
23
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
28
|
const __dirname = path.dirname(__filename);
|
|
25
29
|
// Parse arguments
|
|
@@ -30,20 +34,18 @@ const shouldOpenBrowser = args.includes('--open');
|
|
|
30
34
|
if (!isDebug) {
|
|
31
35
|
console.log = () => { };
|
|
32
36
|
}
|
|
33
|
-
const positionalArgs = args.filter(arg => !arg.startsWith('--'));
|
|
34
|
-
const rootFolderArg = positionalArgs[0];
|
|
35
37
|
const app = new Hono();
|
|
36
38
|
// Configure CORS middleware
|
|
37
39
|
app.use('/*', cors());
|
|
38
40
|
// Initialize managers
|
|
39
|
-
const
|
|
40
|
-
const metadataManager = new MetadataManager(
|
|
41
|
+
const dataDir = getDataDir();
|
|
42
|
+
const metadataManager = new MetadataManager(dataDir);
|
|
41
43
|
const gitManager = new GitManagerImpl();
|
|
42
|
-
const projectManager = new ProjectManagerImpl(rootFolder, metadataManager, gitManager);
|
|
43
|
-
const threadManager = new ThreadManagerImpl(metadataManager, gitManager);
|
|
44
|
-
const conversationManager = new ConversationManagerImpl(rootFolder);
|
|
45
|
-
const neovateExecutor = new NeovateExecutorImpl(metadataManager);
|
|
46
44
|
const processingStateManager = new ProcessingStateManagerImpl();
|
|
45
|
+
const projectManager = new ProjectManagerImpl(dataDir, metadataManager, gitManager, processingStateManager);
|
|
46
|
+
const threadManager = new ThreadManagerImpl(metadataManager, gitManager, processingStateManager);
|
|
47
|
+
const conversationManager = new ConversationManagerImpl(dataDir);
|
|
48
|
+
const neovateExecutor = new NeovateExecutorImpl(metadataManager);
|
|
47
49
|
// Initialize metadata storage
|
|
48
50
|
await metadataManager.initialize();
|
|
49
51
|
// Health check endpoint
|
|
@@ -68,21 +70,34 @@ app.get('/api/threads/processing', (c) => {
|
|
|
68
70
|
});
|
|
69
71
|
// Register API routes
|
|
70
72
|
app.route('/api/projects', createProjectRoutes(projectManager, threadManager));
|
|
73
|
+
app.route('/api/projects', createRunRoutes(projectManager));
|
|
71
74
|
app.route('/api/threads', createThreadRoutes(threadManager, gitManager, conversationManager));
|
|
72
75
|
app.route('/api/chat', createChatRoutes(threadManager, neovateExecutor, conversationManager, processingStateManager));
|
|
73
76
|
app.route('/api/providers', createProviderRoutes(metadataManager));
|
|
74
77
|
app.route('/api/models', createModelRoutes(metadataManager));
|
|
75
78
|
app.route('/api/git', createGitRoutes(metadataManager));
|
|
79
|
+
app.route('/api/onboarding', createOnboardingRoutes(metadataManager));
|
|
80
|
+
app.route('/api/scaffold', createScaffoldRoutes(projectManager));
|
|
76
81
|
// Serve static files from the 'public' directory
|
|
77
82
|
// In production, this will be the built frontend app
|
|
78
83
|
const publicDir = path.join(__dirname, 'public');
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}));
|
|
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
|
+
});
|
|
86
101
|
// Error handling middleware for unmatched routes
|
|
87
102
|
app.all('*', (c) => {
|
|
88
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
|
*/
|
|
@@ -51,9 +51,10 @@ export declare class ConversationManagerImpl implements ConversationManager {
|
|
|
51
51
|
private metadataDir;
|
|
52
52
|
/**
|
|
53
53
|
* Create a new ConversationManagerImpl
|
|
54
|
-
* @param
|
|
54
|
+
* @param dataDir - Directory where metadata files are stored
|
|
55
|
+
* (e.g. ~/Library/Application Support/Tarsk/data)
|
|
55
56
|
*/
|
|
56
|
-
constructor(
|
|
57
|
+
constructor(dataDir: string);
|
|
57
58
|
/**
|
|
58
59
|
* Starts a new conversation message
|
|
59
60
|
*/
|
|
@@ -70,9 +71,6 @@ export declare class ConversationManagerImpl implements ConversationManager {
|
|
|
70
71
|
* Gets the last N messages from conversation history
|
|
71
72
|
*/
|
|
72
73
|
getLastMessages(threadPath: string, count?: number): Promise<ConversationMessage[]>;
|
|
73
|
-
/**
|
|
74
|
-
* Saves conversation history to file
|
|
75
|
-
*/
|
|
76
74
|
private saveConversationHistory;
|
|
77
75
|
/**
|
|
78
76
|
* Gets the file path for a thread's conversation history in the metadata folder
|
|
@@ -10,7 +10,6 @@ import { promises as fs } from 'fs';
|
|
|
10
10
|
import { join, dirname } from 'path';
|
|
11
11
|
import { randomUUID } from 'crypto';
|
|
12
12
|
const CONVERSATION_FILE = 'conversation-history.json';
|
|
13
|
-
const METADATA_DIR = '.metadata';
|
|
14
13
|
/**
|
|
15
14
|
* ConversationManagerImpl provides the implementation for conversation operations
|
|
16
15
|
*/
|
|
@@ -18,10 +17,11 @@ export class ConversationManagerImpl {
|
|
|
18
17
|
metadataDir;
|
|
19
18
|
/**
|
|
20
19
|
* Create a new ConversationManagerImpl
|
|
21
|
-
* @param
|
|
20
|
+
* @param dataDir - Directory where metadata files are stored
|
|
21
|
+
* (e.g. ~/Library/Application Support/Tarsk/data)
|
|
22
22
|
*/
|
|
23
|
-
constructor(
|
|
24
|
-
this.metadataDir =
|
|
23
|
+
constructor(dataDir) {
|
|
24
|
+
this.metadataDir = dataDir;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Starts a new conversation message
|
|
@@ -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;
|
|
@@ -20,9 +21,10 @@ export declare class MetadataManager {
|
|
|
20
21
|
private metadataDir;
|
|
21
22
|
/**
|
|
22
23
|
* Create a new MetadataManager
|
|
23
|
-
* @param
|
|
24
|
+
* @param dataDir - Directory where metadata files will be stored
|
|
25
|
+
* (e.g. ~/Library/Application Support/Tarsk/data)
|
|
24
26
|
*/
|
|
25
|
-
constructor(
|
|
27
|
+
constructor(dataDir: string);
|
|
26
28
|
/**
|
|
27
29
|
* Initialize metadata directory if it doesn't exist
|
|
28
30
|
*/
|
|
@@ -47,15 +49,6 @@ export declare class MetadataManager {
|
|
|
47
49
|
* @returns Array of threads
|
|
48
50
|
*/
|
|
49
51
|
loadThreads(): Promise<Thread[]>;
|
|
50
|
-
/**
|
|
51
|
-
* Perform atomic write using temp file
|
|
52
|
-
*
|
|
53
|
-
* This ensures that if the process crashes during write,
|
|
54
|
-
* we don't end up with corrupted or partial data.
|
|
55
|
-
*
|
|
56
|
-
* @param filePath - Target file path
|
|
57
|
-
* @param data - Data to write
|
|
58
|
-
*/
|
|
59
52
|
private atomicWrite;
|
|
60
53
|
/**
|
|
61
54
|
* Get the metadata directory path
|
|
@@ -87,12 +80,12 @@ export declare class MetadataManager {
|
|
|
87
80
|
/**
|
|
88
81
|
* Update provider keys
|
|
89
82
|
* @param providerName - Name of the provider
|
|
90
|
-
* @param apiKey - API key to save
|
|
83
|
+
* @param apiKey - API key to save (will be encrypted)
|
|
91
84
|
*/
|
|
92
85
|
saveProviderKey(providerName: string, apiKey: string): Promise<void>;
|
|
93
86
|
/**
|
|
94
87
|
* Get all provider keys
|
|
95
|
-
* @returns Record of provider names to API keys
|
|
88
|
+
* @returns Record of provider names to decrypted API keys
|
|
96
89
|
*/
|
|
97
90
|
getProviderKeys(): Promise<Record<string, string>>;
|
|
98
91
|
/**
|
|
@@ -134,6 +127,16 @@ export declare class MetadataManager {
|
|
|
134
127
|
* @param modelIds - Array of model IDs to enable
|
|
135
128
|
*/
|
|
136
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>;
|
|
137
140
|
}
|
|
138
141
|
export {};
|
|
139
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;
|
|
@@ -13,10 +14,11 @@ export class MetadataManager {
|
|
|
13
14
|
metadataDir;
|
|
14
15
|
/**
|
|
15
16
|
* Create a new MetadataManager
|
|
16
|
-
* @param
|
|
17
|
+
* @param dataDir - Directory where metadata files will be stored
|
|
18
|
+
* (e.g. ~/Library/Application Support/Tarsk/data)
|
|
17
19
|
*/
|
|
18
|
-
constructor(
|
|
19
|
-
this.metadataDir =
|
|
20
|
+
constructor(dataDir) {
|
|
21
|
+
this.metadataDir = dataDir;
|
|
20
22
|
this.projectsFile = join(this.metadataDir, "projects.json");
|
|
21
23
|
this.threadsFile = join(this.metadataDir, "threads.json");
|
|
22
24
|
this.stateFile = join(this.metadataDir, "state.json");
|
|
@@ -48,6 +50,7 @@ export class MetadataManager {
|
|
|
48
50
|
selectedThreadId: null,
|
|
49
51
|
providerKeys: {},
|
|
50
52
|
enabledModels: {},
|
|
53
|
+
onboardingCompleted: false,
|
|
51
54
|
});
|
|
52
55
|
}
|
|
53
56
|
}
|
|
@@ -186,11 +189,15 @@ export class MetadataManager {
|
|
|
186
189
|
if (!state.enabledModels) {
|
|
187
190
|
state.enabledModels = {};
|
|
188
191
|
}
|
|
192
|
+
// Ensure onboardingCompleted exists
|
|
193
|
+
if (state.onboardingCompleted === undefined) {
|
|
194
|
+
state.onboardingCompleted = false;
|
|
195
|
+
}
|
|
189
196
|
return state;
|
|
190
197
|
}
|
|
191
198
|
catch (error) {
|
|
192
199
|
if (error.code === "ENOENT") {
|
|
193
|
-
return { selectedThreadId: null, providerKeys: {}, enabledModels: {} };
|
|
200
|
+
return { selectedThreadId: null, providerKeys: {}, enabledModels: {}, onboardingCompleted: false };
|
|
194
201
|
}
|
|
195
202
|
throw new Error(`Failed to load state: ${error}`);
|
|
196
203
|
}
|
|
@@ -201,26 +208,62 @@ export class MetadataManager {
|
|
|
201
208
|
*/
|
|
202
209
|
async saveAllProviderKeys(keys) {
|
|
203
210
|
const state = await this.loadState();
|
|
204
|
-
|
|
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 };
|
|
205
217
|
await this.saveState(state);
|
|
206
218
|
}
|
|
207
219
|
/**
|
|
208
220
|
* Update provider keys
|
|
209
221
|
* @param providerName - Name of the provider
|
|
210
|
-
* @param apiKey - API key to save
|
|
222
|
+
* @param apiKey - API key to save (will be encrypted)
|
|
211
223
|
*/
|
|
212
224
|
async saveProviderKey(providerName, apiKey) {
|
|
213
225
|
const state = await this.loadState();
|
|
214
|
-
|
|
226
|
+
// Encrypt the API key before saving
|
|
227
|
+
state.providerKeys[providerName] = apiKey ? encrypt(apiKey) : '';
|
|
215
228
|
await this.saveState(state);
|
|
216
229
|
}
|
|
217
230
|
/**
|
|
218
231
|
* Get all provider keys
|
|
219
|
-
* @returns Record of provider names to API keys
|
|
232
|
+
* @returns Record of provider names to decrypted API keys
|
|
220
233
|
*/
|
|
221
234
|
async getProviderKeys() {
|
|
222
235
|
const state = await this.loadState();
|
|
223
|
-
|
|
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;
|
|
224
267
|
}
|
|
225
268
|
/**
|
|
226
269
|
* Set the selected thread ID
|
|
@@ -301,5 +344,22 @@ export class MetadataManager {
|
|
|
301
344
|
}
|
|
302
345
|
await this.saveState(state);
|
|
303
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
|
+
}
|
|
304
364
|
}
|
|
305
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
|