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.
Files changed (71) hide show
  1. package/dist/index.js +31 -16
  2. package/dist/lib/conversation-content.d.ts +28 -0
  3. package/dist/lib/conversation-content.js +130 -0
  4. package/dist/lib/response-builder.d.ts +1 -1
  5. package/dist/managers/conversation-manager.d.ts +4 -6
  6. package/dist/managers/conversation-manager.js +4 -4
  7. package/dist/managers/git-manager.d.ts +7 -1
  8. package/dist/managers/git-manager.js +8 -1
  9. package/dist/managers/metadata-manager.d.ts +17 -14
  10. package/dist/managers/metadata-manager.js +69 -9
  11. package/dist/managers/model-manager.d.ts +1 -1
  12. package/dist/managers/neovate-executor.d.ts +1 -1
  13. package/dist/managers/process-manager.d.ts +55 -0
  14. package/dist/managers/process-manager.js +221 -0
  15. package/dist/managers/project-manager.d.ts +129 -6
  16. package/dist/managers/project-manager.js +410 -96
  17. package/dist/managers/thread-manager.d.ts +9 -7
  18. package/dist/managers/thread-manager.js +97 -28
  19. package/dist/paths.d.ts +22 -0
  20. package/dist/paths.js +26 -0
  21. package/dist/provider-data.d.ts +13 -13
  22. package/dist/provider-data.js +26 -9
  23. package/dist/public/assets/index-BLGMN24v.css +1 -0
  24. package/dist/public/assets/index-ClP26kjT.js +86 -0
  25. package/dist/public/index.html +2 -2
  26. package/dist/public/template-types/logo-angular.svg +33 -0
  27. package/dist/public/template-types/logo-astro.svg +11 -0
  28. package/dist/public/template-types/logo-capacitor.svg +8 -0
  29. package/dist/public/template-types/logo-hydrogen.svg +1 -0
  30. package/dist/public/template-types/logo-lit.svg +1 -0
  31. package/dist/public/template-types/logo-nextjs.svg +11 -0
  32. package/dist/public/template-types/logo-nuxt.svg +3 -0
  33. package/dist/public/template-types/logo-preact.svg +6 -0
  34. package/dist/public/template-types/logo-qwik.svg +5 -0
  35. package/dist/public/template-types/logo-react.svg +1 -0
  36. package/dist/public/template-types/logo-solid.svg +1 -0
  37. package/dist/public/template-types/logo-svelte.svg +15 -0
  38. package/dist/public/template-types/logo-vite.svg +15 -0
  39. package/dist/public/template-types/logo-vue.svg +1 -0
  40. package/dist/public/template-types/logo-waku.svg +19 -0
  41. package/dist/public/template-types/logo-web.svg +14 -0
  42. package/dist/routes/chat.js +21 -10
  43. package/dist/routes/git.js +173 -0
  44. package/dist/routes/onboarding.d.ts +17 -0
  45. package/dist/routes/onboarding.js +94 -0
  46. package/dist/routes/projects.js +162 -23
  47. package/dist/routes/run.d.ts +18 -0
  48. package/dist/routes/run.js +180 -0
  49. package/dist/routes/scaffold.d.ts +10 -0
  50. package/dist/routes/scaffold.js +48 -0
  51. package/dist/routes/threads.js +44 -45
  52. package/dist/scaffold/index.d.ts +7 -0
  53. package/dist/scaffold/index.js +5 -0
  54. package/dist/scaffold/runner.d.ts +46 -0
  55. package/dist/scaffold/runner.js +378 -0
  56. package/dist/scaffold/types.d.ts +24 -0
  57. package/dist/scaffold/types.js +5 -0
  58. package/dist/scaffold-templates.json +559 -0
  59. package/dist/utils/crypto.d.ts +29 -0
  60. package/dist/utils/crypto.js +122 -0
  61. package/dist/utils/open-router-models.d.ts +1 -1
  62. package/dist/utils/openai-models.d.ts +1 -1
  63. package/dist/utils/run-command-detector.d.ts +26 -0
  64. package/dist/utils/run-command-detector.js +98 -0
  65. package/package.json +7 -4
  66. package/dist/public/assets/index-CLr9LKtA.js +0 -8503
  67. package/dist/public/assets/index-CjXGVbI7.css +0 -1
  68. package/dist/public/placeholder-logo.svg +0 -1
  69. package/dist/public/placeholder.svg +0 -1
  70. package/dist/types/models.d.ts +0 -315
  71. 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 { AVAILABLE_PROGRAMS } from './types/models.js';
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 rootFolder = rootFolderArg ? path.resolve(process.cwd(), rootFolderArg) : (process.env.ROOT_FOLDER || process.cwd());
40
- const metadataManager = new MetadataManager(rootFolder);
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
- app.use('/*', serveStatic({
80
- root: path.relative(process.cwd(), publicDir)
81
- }));
82
- // Fallback to index.html for SPA routing
83
- app.get('*', serveStatic({
84
- path: path.relative(process.cwd(), path.join(publicDir, 'index.html'))
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 '../types/models.js';
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 '../types/models.js';
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 rootFolder - Base directory where metadata will be stored
54
+ * @param dataDir - Directory where metadata files are stored
55
+ * (e.g. ~/Library/Application Support/Tarsk/data)
55
56
  */
56
- constructor(rootFolder?: string);
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 rootFolder - Base directory where metadata will be stored
20
+ * @param dataDir - Directory where metadata files are stored
21
+ * (e.g. ~/Library/Application Support/Tarsk/data)
22
22
  */
23
- constructor(rootFolder = './projects') {
24
- this.metadataDir = join(rootFolder, METADATA_DIR);
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 '../types/models.js';
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 "../types/models.js";
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 rootFolder - Base directory where metadata will be stored
24
+ * @param dataDir - Directory where metadata files will be stored
25
+ * (e.g. ~/Library/Application Support/Tarsk/data)
24
26
  */
25
- constructor(rootFolder: string);
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 rootFolder - Base directory where metadata will be stored
17
+ * @param dataDir - Directory where metadata files will be stored
18
+ * (e.g. ~/Library/Application Support/Tarsk/data)
17
19
  */
18
- constructor(rootFolder) {
19
- this.metadataDir = join(rootFolder, ".metadata");
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
- state.providerKeys = { ...state.providerKeys, ...keys };
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
- state.providerKeys[providerName] = apiKey;
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
- return state.providerKeys;
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 '../types/models.js';
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 "../types/models.js";
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