tarsk 0.3.16 → 0.3.19

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 CHANGED
@@ -20,6 +20,7 @@ import { createProviderRoutes } from './routes/providers.js';
20
20
  import { createModelRoutes } from './routes/models.js';
21
21
  import { createGitRoutes } from './routes/git.js';
22
22
  import { AVAILABLE_PROGRAMS } from './types/models.js';
23
+ import { getDataDir } from './paths.js';
23
24
  const __filename = fileURLToPath(import.meta.url);
24
25
  const __dirname = path.dirname(__filename);
25
26
  // Parse arguments
@@ -30,20 +31,18 @@ const shouldOpenBrowser = args.includes('--open');
30
31
  if (!isDebug) {
31
32
  console.log = () => { };
32
33
  }
33
- const positionalArgs = args.filter(arg => !arg.startsWith('--'));
34
- const rootFolderArg = positionalArgs[0];
35
34
  const app = new Hono();
36
35
  // Configure CORS middleware
37
36
  app.use('/*', cors());
38
37
  // Initialize managers
39
- const rootFolder = rootFolderArg ? path.resolve(process.cwd(), rootFolderArg) : (process.env.ROOT_FOLDER || process.cwd());
40
- const metadataManager = new MetadataManager(rootFolder);
38
+ const dataDir = getDataDir();
39
+ const metadataManager = new MetadataManager(dataDir);
41
40
  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
41
  const processingStateManager = new ProcessingStateManagerImpl();
42
+ const projectManager = new ProjectManagerImpl(dataDir, metadataManager, gitManager, processingStateManager);
43
+ const threadManager = new ThreadManagerImpl(metadataManager, gitManager, processingStateManager);
44
+ const conversationManager = new ConversationManagerImpl(dataDir);
45
+ const neovateExecutor = new NeovateExecutorImpl(metadataManager);
47
46
  // Initialize metadata storage
48
47
  await metadataManager.initialize();
49
48
  // Health check endpoint
@@ -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
  */
@@ -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
@@ -20,9 +20,10 @@ export declare class MetadataManager {
20
20
  private metadataDir;
21
21
  /**
22
22
  * Create a new MetadataManager
23
- * @param rootFolder - Base directory where metadata will be stored
23
+ * @param dataDir - Directory where metadata files will be stored
24
+ * (e.g. ~/Library/Application Support/Tarsk/data)
24
25
  */
25
- constructor(rootFolder: string);
26
+ constructor(dataDir: string);
26
27
  /**
27
28
  * Initialize metadata directory if it doesn't exist
28
29
  */
@@ -13,10 +13,11 @@ export class MetadataManager {
13
13
  metadataDir;
14
14
  /**
15
15
  * Create a new MetadataManager
16
- * @param rootFolder - Base directory where metadata will be stored
16
+ * @param dataDir - Directory where metadata files will be stored
17
+ * (e.g. ~/Library/Application Support/Tarsk/data)
17
18
  */
18
- constructor(rootFolder) {
19
- this.metadataDir = join(rootFolder, ".metadata");
19
+ constructor(dataDir) {
20
+ this.metadataDir = dataDir;
20
21
  this.projectsFile = join(this.metadataDir, "projects.json");
21
22
  this.threadsFile = join(this.metadataDir, "threads.json");
22
23
  this.stateFile = join(this.metadataDir, "state.json");
@@ -10,6 +10,7 @@
10
10
  import { Project, ProjectEvent, Command } from '../types/models.js';
11
11
  import { MetadataManager } from './metadata-manager.js';
12
12
  import { GitManager } from './git-manager.js';
13
+ import type { ProcessingStateManager } from './processing-state-manager.js';
13
14
  /**
14
15
  * ProjectManager interface defines the contract for project operations
15
16
  */
@@ -67,6 +68,12 @@ export interface ProjectManager {
67
68
  * @param cwd - Optional working directory relative to thread path
68
69
  */
69
70
  runCommand(threadId: string, commandLine: string, cwd?: string): AsyncGenerator<unknown>;
71
+ /**
72
+ * Updates the project's setup script
73
+ * @param projectId - The project ID
74
+ * @param setupScript - The setup script to run after clone (or null to clear)
75
+ */
76
+ updateSetupScript(projectId: string, setupScript: string | null): Promise<void>;
70
77
  }
71
78
  /**
72
79
  * ProjectManagerImpl provides the implementation for project operations
@@ -75,17 +82,19 @@ export declare class ProjectManagerImpl implements ProjectManager {
75
82
  private rootFolder;
76
83
  private metadataManager;
77
84
  private gitManager;
85
+ private processingStateManager?;
78
86
  /**
79
87
  * Create a new ProjectManager
80
88
  * @param rootFolder - Base directory where projects will be stored
81
89
  * @param metadataManager - Manager for persisting metadata
82
90
  * @param gitManager - Manager for git operations
91
+ * @param processingStateManager - Optional manager for processing state (for spinner during clone)
83
92
  *
84
93
  * Requirements:
85
94
  * - 1.1 - WHEN a user provides a git URL to create a new Project, THE CLI SHALL clone the repository into a folder under the Root_Folder
86
95
  * - 1.4 - THE CLI SHALL determine the storage path for each Project folder
87
96
  */
88
- constructor(rootFolder: string, metadataManager: MetadataManager, gitManager: GitManager);
97
+ constructor(rootFolder: string, metadataManager: MetadataManager, gitManager: GitManager, processingStateManager?: ProcessingStateManager);
89
98
  /**
90
99
  * Creates a new project from a git URL
91
100
  *
@@ -149,9 +158,9 @@ export declare class ProjectManagerImpl implements ProjectManager {
149
158
  */
150
159
  private deriveProjectName;
151
160
  /**
152
- * Generates a project path under the root folder
161
+ * Generates a project path under the root folder using the project ID
153
162
  *
154
- * @param projectName - The project name
163
+ * @param projectId - The unique project ID
155
164
  * @returns The absolute path to the project folder
156
165
  *
157
166
  * Requirements:
@@ -189,6 +198,19 @@ export declare class ProjectManagerImpl implements ProjectManager {
189
198
  * @param commandId - The ID of the command to delete
190
199
  */
191
200
  deleteCommand(projectId: string, commandId: string): Promise<void>;
201
+ /**
202
+ * Detects a suggested setup script by checking for package.json and lock files
203
+ * Order: yarn.lock, bun.lock, pnpm-lock.yaml, else npm install
204
+ * @param chrootPath - Path to the cloned repo root
205
+ * @returns The suggested install command or null if no package.json
206
+ */
207
+ private detectSetupScript;
208
+ /**
209
+ * Updates the project's setup script
210
+ * @param projectId - The project ID
211
+ * @param setupScript - The setup script to run after clone (or null to clear)
212
+ */
213
+ updateSetupScript(projectId: string, setupScript: string | null): Promise<void>;
192
214
  /**
193
215
  * Runs a command in a thread's directory
194
216
  * @param threadId - The thread ID where to run the command
@@ -10,6 +10,7 @@
10
10
  import { randomUUID } from 'crypto';
11
11
  import { join, relative } from 'path';
12
12
  import { realpathSync } from 'fs';
13
+ import { access } from 'fs/promises';
13
14
  /**
14
15
  * ProjectManagerImpl provides the implementation for project operations
15
16
  */
@@ -17,20 +18,23 @@ export class ProjectManagerImpl {
17
18
  rootFolder;
18
19
  metadataManager;
19
20
  gitManager;
21
+ processingStateManager;
20
22
  /**
21
23
  * Create a new ProjectManager
22
24
  * @param rootFolder - Base directory where projects will be stored
23
25
  * @param metadataManager - Manager for persisting metadata
24
26
  * @param gitManager - Manager for git operations
27
+ * @param processingStateManager - Optional manager for processing state (for spinner during clone)
25
28
  *
26
29
  * Requirements:
27
30
  * - 1.1 - WHEN a user provides a git URL to create a new Project, THE CLI SHALL clone the repository into a folder under the Root_Folder
28
31
  * - 1.4 - THE CLI SHALL determine the storage path for each Project folder
29
32
  */
30
- constructor(rootFolder, metadataManager, gitManager) {
33
+ constructor(rootFolder, metadataManager, gitManager, processingStateManager) {
31
34
  this.rootFolder = rootFolder;
32
35
  this.metadataManager = metadataManager;
33
36
  this.gitManager = gitManager;
37
+ this.processingStateManager = processingStateManager;
34
38
  }
35
39
  /**
36
40
  * Creates a new project from a git URL
@@ -63,27 +67,28 @@ export class ProjectManagerImpl {
63
67
  };
64
68
  return;
65
69
  }
70
+ let initialThreadId;
66
71
  try {
67
72
  // Generate unique project ID
68
73
  const projectId = randomUUID();
69
74
  // Derive project name from git URL
70
75
  const projectName = this.deriveProjectName(gitUrl);
71
- // Generate project path
72
- const projectPath = this.generateProjectPath(projectName);
73
- yield {
74
- type: 'progress',
75
- message: `Creating project "${projectName}" from ${gitUrl}...`
76
- };
77
- // Generate unique thread ID for the initial thread
78
- const initialThreadId = randomUUID();
76
+ // Generate project path using the unique ID (not the name, to avoid collisions)
77
+ const projectPath = this.generateProjectPath(projectId);
78
+ // Generate unique thread ID for the initial thread (before clone so frontend can show spinner)
79
+ initialThreadId = randomUUID();
79
80
  const firstThreadPath = join(projectPath, initialThreadId);
81
+ this.processingStateManager?.setProcessing(initialThreadId);
80
82
  yield {
81
83
  type: 'progress',
82
- message: `Cloning repository to ${firstThreadPath}...`
84
+ message: `Creating project "${projectName}" from ${gitUrl}...`,
85
+ threadId: initialThreadId,
86
+ projectId
83
87
  };
84
88
  // Stream git clone events
85
89
  for await (const gitEvent of this.gitManager.cloneRepository(gitUrl, firstThreadPath)) {
86
90
  if (gitEvent.type === 'error') {
91
+ this.processingStateManager?.clearProcessing(initialThreadId);
87
92
  yield {
88
93
  type: 'error',
89
94
  message: `Git clone failed: ${gitEvent.message}`
@@ -98,7 +103,7 @@ export class ProjectManagerImpl {
98
103
  }
99
104
  }
100
105
  // Create and checkout branch for initial thread
101
- const initialThreadTitle = 'Thread 1';
106
+ const initialThreadTitle = 'thread-1';
102
107
  let branchName = this.gitManager.sanitizeBranchName(initialThreadTitle);
103
108
  yield {
104
109
  type: 'progress',
@@ -122,6 +127,7 @@ export class ProjectManagerImpl {
122
127
  // Create and checkout the new branch
123
128
  for await (const gitEvent of this.gitManager.createAndCheckoutBranch(firstThreadPath, branchName)) {
124
129
  if (gitEvent.type === 'error') {
130
+ this.processingStateManager?.clearProcessing(initialThreadId);
125
131
  yield {
126
132
  type: 'error',
127
133
  message: `Failed to create initial branch: ${gitEvent.message}`
@@ -139,6 +145,8 @@ export class ProjectManagerImpl {
139
145
  type: 'progress',
140
146
  message: `Branch "${branchName}" created and checked out`
141
147
  };
148
+ // Detect suggested setup script from package.json and lock files
149
+ const suggestedSetupScript = await this.detectSetupScript(firstThreadPath);
142
150
  // Create project metadata
143
151
  const project = {
144
152
  id: projectId,
@@ -169,13 +177,18 @@ export class ProjectManagerImpl {
169
177
  message: `Project "${projectName}" created successfully with initial thread`
170
178
  };
171
179
  // Yield complete event with project data
180
+ this.processingStateManager?.clearProcessing(initialThreadId);
172
181
  yield {
173
182
  type: 'complete',
174
183
  message: 'Project creation complete',
175
- project
184
+ project,
185
+ suggestedSetupScript: suggestedSetupScript ?? undefined
176
186
  };
177
187
  }
178
188
  catch (error) {
189
+ if (typeof initialThreadId !== 'undefined') {
190
+ this.processingStateManager?.clearProcessing(initialThreadId);
191
+ }
179
192
  yield {
180
193
  type: 'error',
181
194
  message: `Failed to create project: ${error instanceof Error ? error.message : String(error)}`
@@ -258,16 +271,16 @@ export class ProjectManagerImpl {
258
271
  return name;
259
272
  }
260
273
  /**
261
- * Generates a project path under the root folder
274
+ * Generates a project path under the root folder using the project ID
262
275
  *
263
- * @param projectName - The project name
276
+ * @param projectId - The unique project ID
264
277
  * @returns The absolute path to the project folder
265
278
  *
266
279
  * Requirements:
267
280
  * - 1.4 - THE CLI SHALL determine the storage path for each Project folder
268
281
  */
269
- generateProjectPath(projectName) {
270
- return join(this.rootFolder, projectName);
282
+ generateProjectPath(projectId) {
283
+ return join(this.rootFolder, projectId);
271
284
  }
272
285
  /**
273
286
  * Gets the root folder path
@@ -391,6 +404,62 @@ export class ProjectManagerImpl {
391
404
  }
392
405
  await this.metadataManager.saveProjects(projects);
393
406
  }
407
+ /**
408
+ * Detects a suggested setup script by checking for package.json and lock files
409
+ * Order: yarn.lock, bun.lock, pnpm-lock.yaml, else npm install
410
+ * @param chrootPath - Path to the cloned repo root
411
+ * @returns The suggested install command or null if no package.json
412
+ */
413
+ async detectSetupScript(chrootPath) {
414
+ try {
415
+ await access(join(chrootPath, 'package.json'));
416
+ }
417
+ catch {
418
+ return null;
419
+ }
420
+ try {
421
+ await access(join(chrootPath, 'yarn.lock'));
422
+ return 'yarn install';
423
+ }
424
+ catch {
425
+ // continue
426
+ }
427
+ try {
428
+ await access(join(chrootPath, 'bun.lock'));
429
+ return 'bun install';
430
+ }
431
+ catch {
432
+ // continue
433
+ }
434
+ try {
435
+ await access(join(chrootPath, 'pnpm-lock.yaml'));
436
+ return 'pnpm install';
437
+ }
438
+ catch {
439
+ // continue
440
+ }
441
+ return 'npm install';
442
+ }
443
+ /**
444
+ * Updates the project's setup script
445
+ * @param projectId - The project ID
446
+ * @param setupScript - The setup script to run after clone (or null to clear)
447
+ */
448
+ async updateSetupScript(projectId, setupScript) {
449
+ const projects = await this.metadataManager.loadProjects();
450
+ const projectIndex = projects.findIndex(p => p.id === projectId);
451
+ if (projectIndex === -1) {
452
+ throw new Error(`Project not found: ${projectId}`);
453
+ }
454
+ // Update the project's setup script
455
+ if (setupScript === null || setupScript === '') {
456
+ delete projects[projectIndex].setupScript;
457
+ }
458
+ else {
459
+ projects[projectIndex].setupScript = setupScript;
460
+ }
461
+ await this.metadataManager.saveProjects(projects);
462
+ }
394
463
  /**
395
464
  * Runs a command in a thread's directory
396
465
  * @param threadId - The thread ID where to run the command
@@ -11,6 +11,7 @@
11
11
  import { Thread, ThreadEvent } from '../types/models.js';
12
12
  import { MetadataManager } from './metadata-manager.js';
13
13
  import { GitManager } from './git-manager.js';
14
+ import type { ProcessingStateManager } from './processing-state-manager.js';
14
15
  /**
15
16
  * ThreadManager interface defines the contract for thread operations
16
17
  */
@@ -68,16 +69,18 @@ export interface ThreadManager {
68
69
  export declare class ThreadManagerImpl implements ThreadManager {
69
70
  private metadataManager;
70
71
  private gitManager;
72
+ private processingStateManager?;
71
73
  /**
72
74
  * Create a new ThreadManager
73
75
  * @param metadataManager - Manager for persisting metadata
74
76
  * @param gitManager - Manager for git operations
77
+ * @param processingStateManager - Optional manager for processing state (for spinner during clone)
75
78
  *
76
79
  * Requirements:
77
80
  * - 2.1 - WHEN a user creates a new Thread for a Project, THE CLI SHALL create a new clone of the Project's git repository
78
81
  * - 2.4 - THE System SHALL maintain a title for each Thread
79
82
  */
80
- constructor(metadataManager: MetadataManager, gitManager: GitManager);
83
+ constructor(metadataManager: MetadataManager, gitManager: GitManager, processingStateManager?: ProcessingStateManager);
81
84
  /**
82
85
  * Creates a new thread for an existing project
83
86
  *
@@ -157,6 +160,13 @@ export declare class ThreadManagerImpl implements ThreadManager {
157
160
  * @returns The selected thread ID or null
158
161
  */
159
162
  getSelectedThreadId(): Promise<string | null>;
163
+ /**
164
+ * Runs a setup script in the given path and yields progress events
165
+ * @param threadPath - Path to run the command in
166
+ * @param commandLine - The command to execute
167
+ * @yields ThreadEvent progress events
168
+ */
169
+ private runSetupScript;
160
170
  /**
161
171
  * Generates a unique thread path
162
172
  *
@@ -175,12 +185,10 @@ export declare class ThreadManagerImpl implements ThreadManager {
175
185
  /**
176
186
  * Generates a thread title if not provided
177
187
  *
178
- * Format: project-name Thread word1-word2
188
+ * Format: thread-{n} where n starts at 1 and increments for each new thread
179
189
  *
180
- * @param existingTitles - Set of titles already in use
181
- * @param threadId - Thread ID used as a fallback
182
- * @param projectName - Optional project name to include in title
183
- * @returns The generated thread title
190
+ * @param existingTitles - Set of titles already in use for this project
191
+ * @returns The generated thread title (e.g. thread-1, thread-2, thread-3)
184
192
  */
185
193
  private generateThreadTitle;
186
194
  /**
@@ -16,18 +16,21 @@ import { join } from 'path';
16
16
  export class ThreadManagerImpl {
17
17
  metadataManager;
18
18
  gitManager;
19
+ processingStateManager;
19
20
  /**
20
21
  * Create a new ThreadManager
21
22
  * @param metadataManager - Manager for persisting metadata
22
23
  * @param gitManager - Manager for git operations
24
+ * @param processingStateManager - Optional manager for processing state (for spinner during clone)
23
25
  *
24
26
  * Requirements:
25
27
  * - 2.1 - WHEN a user creates a new Thread for a Project, THE CLI SHALL create a new clone of the Project's git repository
26
28
  * - 2.4 - THE System SHALL maintain a title for each Thread
27
29
  */
28
- constructor(metadataManager, gitManager) {
30
+ constructor(metadataManager, gitManager, processingStateManager) {
29
31
  this.metadataManager = metadataManager;
30
32
  this.gitManager = gitManager;
33
+ this.processingStateManager = processingStateManager;
31
34
  }
32
35
  /**
33
36
  * Creates a new thread for an existing project
@@ -53,6 +56,7 @@ export class ThreadManagerImpl {
53
56
  * - 7.4 - THE CLI SHALL ensure each Thread clone is stored in a unique directory path
54
57
  */
55
58
  async *createThread(projectId, title) {
59
+ let threadId;
56
60
  try {
57
61
  // Load project to verify it exists and get git URL
58
62
  const projects = await this.metadataManager.loadProjects();
@@ -65,7 +69,7 @@ export class ThreadManagerImpl {
65
69
  return;
66
70
  }
67
71
  // Generate unique thread ID
68
- const threadId = randomUUID();
72
+ threadId = randomUUID();
69
73
  // Generate thread path: {projectPath}/{threadId}
70
74
  const threadPath = this.generateThreadPath(project.path, threadId);
71
75
  // Generate thread title if not provided
@@ -73,10 +77,12 @@ export class ThreadManagerImpl {
73
77
  const existingTitles = new Set(existingThreads
74
78
  .filter(t => t.projectId === projectId)
75
79
  .map(t => t.title));
76
- const threadTitle = title || this.generateThreadTitle(existingTitles, threadId, project.name);
80
+ const threadTitle = title || this.generateThreadTitle(existingTitles);
81
+ this.processingStateManager?.setProcessing(threadId);
77
82
  yield {
78
83
  type: 'progress',
79
- message: `Creating thread "${threadTitle}" for project "${project.name}"...`
84
+ message: `Creating thread "${threadTitle}" for project "${project.name}"...`,
85
+ threadId
80
86
  };
81
87
  yield {
82
88
  type: 'progress',
@@ -85,6 +91,7 @@ export class ThreadManagerImpl {
85
91
  // Stream git clone events
86
92
  for await (const gitEvent of this.gitManager.cloneRepository(project.gitUrl, threadPath)) {
87
93
  if (gitEvent.type === 'error') {
94
+ this.processingStateManager?.clearProcessing(threadId);
88
95
  yield {
89
96
  type: 'error',
90
97
  message: `Git clone failed: ${gitEvent.message}`
@@ -122,6 +129,7 @@ export class ThreadManagerImpl {
122
129
  // Create and checkout the new branch
123
130
  for await (const gitEvent of this.gitManager.createAndCheckoutBranch(threadPath, branchName)) {
124
131
  if (gitEvent.type === 'error') {
132
+ this.processingStateManager?.clearProcessing(threadId);
125
133
  yield {
126
134
  type: 'error',
127
135
  message: `Failed to create branch: ${gitEvent.message}`
@@ -139,6 +147,16 @@ export class ThreadManagerImpl {
139
147
  type: 'progress',
140
148
  message: `Branch "${branchName}" created and checked out`
141
149
  };
150
+ // Run setup script if project has one configured
151
+ if (project.setupScript) {
152
+ yield {
153
+ type: 'progress',
154
+ message: `Running setup script: ${project.setupScript}`
155
+ };
156
+ for await (const event of this.runSetupScript(threadPath, project.setupScript)) {
157
+ yield event;
158
+ }
159
+ }
142
160
  // Create thread metadata
143
161
  const thread = {
144
162
  id: threadId,
@@ -161,6 +179,7 @@ export class ThreadManagerImpl {
161
179
  message: `Thread "${threadTitle}" created successfully`
162
180
  };
163
181
  // Yield complete event with thread data
182
+ this.processingStateManager?.clearProcessing(threadId);
164
183
  yield {
165
184
  type: 'complete',
166
185
  message: 'Thread creation complete',
@@ -168,6 +187,9 @@ export class ThreadManagerImpl {
168
187
  };
169
188
  }
170
189
  catch (error) {
190
+ if (typeof threadId !== 'undefined') {
191
+ this.processingStateManager?.clearProcessing(threadId);
192
+ }
171
193
  yield {
172
194
  type: 'error',
173
195
  message: `Failed to create thread: ${error instanceof Error ? error.message : String(error)}`
@@ -272,6 +294,64 @@ export class ThreadManagerImpl {
272
294
  async getSelectedThreadId() {
273
295
  return await this.metadataManager.getSelectedThread();
274
296
  }
297
+ /**
298
+ * Runs a setup script in the given path and yields progress events
299
+ * @param threadPath - Path to run the command in
300
+ * @param commandLine - The command to execute
301
+ * @yields ThreadEvent progress events
302
+ */
303
+ async *runSetupScript(threadPath, commandLine) {
304
+ const { spawn } = await import('child_process');
305
+ const child = spawn(commandLine, {
306
+ shell: true,
307
+ cwd: threadPath
308
+ });
309
+ const decoder = new TextDecoder();
310
+ let resolveNext = () => { };
311
+ const outputQueue = [];
312
+ const errorQueue = [];
313
+ child.stdout.on('data', (data) => {
314
+ outputQueue.push(decoder.decode(data));
315
+ resolveNext();
316
+ });
317
+ child.stderr.on('data', (data) => {
318
+ errorQueue.push(decoder.decode(data));
319
+ resolveNext();
320
+ });
321
+ let isDone = false;
322
+ child.on('close', (code) => {
323
+ if (code !== 0) {
324
+ errorQueue.push(`Process exited with code ${code}`);
325
+ }
326
+ isDone = true;
327
+ resolveNext();
328
+ });
329
+ child.on('error', (err) => {
330
+ errorQueue.push(err.message);
331
+ isDone = true;
332
+ resolveNext();
333
+ });
334
+ const waitForData = () => new Promise((resolve) => {
335
+ resolveNext = resolve;
336
+ });
337
+ while (!isDone || outputQueue.length > 0 || errorQueue.length > 0) {
338
+ if (outputQueue.length > 0) {
339
+ const text = outputQueue.shift();
340
+ for (const line of text.split('\n').filter(l => l.trim())) {
341
+ yield { type: 'progress', message: line };
342
+ }
343
+ }
344
+ else if (errorQueue.length > 0) {
345
+ const text = errorQueue.shift();
346
+ for (const line of text.split('\n').filter(l => l.trim())) {
347
+ yield { type: 'progress', message: line };
348
+ }
349
+ }
350
+ else {
351
+ await waitForData();
352
+ }
353
+ }
354
+ }
275
355
  /**
276
356
  * Generates a unique thread path
277
357
  *
@@ -292,34 +372,23 @@ export class ThreadManagerImpl {
292
372
  /**
293
373
  * Generates a thread title if not provided
294
374
  *
295
- * Format: project-name Thread word1-word2
375
+ * Format: thread-{n} where n starts at 1 and increments for each new thread
296
376
  *
297
- * @param existingTitles - Set of titles already in use
298
- * @param threadId - Thread ID used as a fallback
299
- * @param projectName - Optional project name to include in title
300
- * @returns The generated thread title
377
+ * @param existingTitles - Set of titles already in use for this project
378
+ * @returns The generated thread title (e.g. thread-1, thread-2, thread-3)
301
379
  */
302
- generateThreadTitle(existingTitles, threadId, projectName) {
303
- const wordsA = [
304
- 'amber', 'brisk', 'calm', 'drift', 'ember', 'frost', 'glow', 'hollow',
305
- 'ivory', 'jolly', 'keen', 'lucid', 'mellow', 'noble', 'opal', 'prism',
306
- 'quiet', 'ripple', 'sunny', 'timber', 'ultra', 'vivid', 'wisp', 'zen'
307
- ];
308
- const wordsB = [
309
- 'arch', 'bay', 'crest', 'dune', 'echo', 'fjord', 'glen', 'harbor',
310
- 'isle', 'jewel', 'knoll', 'lagoon', 'mesa', 'nest', 'oasis', 'peak',
311
- 'quarry', 'reef', 'strand', 'trail', 'vale', 'wharf', 'yard', 'zephyr'
312
- ];
313
- const prefix = projectName ? `${projectName} Thread ` : 'Thread ';
314
- for (let attempt = 0; attempt < 12; attempt += 1) {
315
- const word1 = wordsA[Math.floor(Math.random() * wordsA.length)];
316
- const word2 = wordsB[Math.floor(Math.random() * wordsB.length)];
317
- const candidate = `${prefix}${word1}-${word2}`;
318
- if (!existingTitles.has(candidate)) {
319
- return candidate;
380
+ generateThreadTitle(existingTitles) {
381
+ const threadNumRegex = /^thread-(\d+)$/i;
382
+ let maxNum = 0;
383
+ for (const title of existingTitles) {
384
+ const match = title.match(threadNumRegex);
385
+ if (match) {
386
+ const n = parseInt(match[1], 10);
387
+ if (n > maxNum)
388
+ maxNum = n;
320
389
  }
321
390
  }
322
- return `${prefix}${threadId.slice(0, 8)}`;
391
+ return `Thread ${maxNum + 1}`;
323
392
  }
324
393
  /**
325
394
  * Lists all files in a thread's directory (tracked and untracked, but not ignored)
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Centralized path configuration for Tarsk
3
+ *
4
+ * Application data is stored in ~/Library/Application Support/Tarsk/data/
5
+ * This includes projects.json, threads.json, state.json, and conversations.
6
+ */
7
+ /**
8
+ * The root application support directory for Tarsk.
9
+ * On macOS: ~/Library/Application Support/Tarsk
10
+ */
11
+ export declare const APP_SUPPORT_DIR: string;
12
+ /**
13
+ * The data directory where metadata files are stored.
14
+ * ~/Library/Application Support/Tarsk/data
15
+ */
16
+ export declare const DATA_DIR: string;
17
+ /**
18
+ * Get the data directory path for storing metadata.
19
+ * @returns The absolute path to ~/Library/Application Support/Tarsk/data
20
+ */
21
+ export declare function getDataDir(): string;
22
+ //# sourceMappingURL=paths.d.ts.map