@suco/su-auggie-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Debug tool - inspect internal server state (only available with --debug)
3
+ */
4
+ import { z } from 'zod';
5
+ import { workspaceManager } from '../workspace-manager.js';
6
+ import { createLogger } from '../logger.js';
7
+ import { INDEXING_CONFIG, WATCHER_CONFIG, PERSISTENCE_CONFIG, isPersistenceEnabled } from '../config.js';
8
+ import { sendLogToClient, clientSupportsElicitation, elicitInput, getMcpServer, } from '../mcp-notifications.js';
9
+ const logger = createLogger('DebugTool');
10
+ /** Tool name */
11
+ export const DEBUG_TOOL_NAME = 'debug';
12
+ /** Tool description */
13
+ export const DEBUG_TOOL_DESCRIPTION = `Inspect internal server state for troubleshooting.
14
+
15
+ Actions:
16
+ - state: Dump full internal state (contexts, watchers, queues)
17
+ - stats: Index statistics (file counts, sizes, timings)
18
+ - config: Show current configuration values
19
+ - capabilities: Show client MCP capabilities (elicitation, logging, etc.)
20
+ - emit: Test MCP client notifications (logging or elicitation)
21
+
22
+ Parameters:
23
+ - action: Required action to perform
24
+ - root: Workspace root path for workspace-specific info (optional)
25
+ - type: For emit action: "logging" or "elicitation"
26
+ - message: Message for emit action (optional)`;
27
+ /** Input schema for zod validation */
28
+ export const debugInputSchema = z.object({
29
+ action: z.enum(['state', 'stats', 'config', 'capabilities', 'emit']),
30
+ root: z.string().optional(),
31
+ type: z.enum(['logging', 'elicitation']).optional(),
32
+ message: z.string().optional(),
33
+ });
34
+ /** Zod schema shape for MCP tool registration */
35
+ export const DEBUG_PARAMS_SHAPE = {
36
+ action: z.enum(['state', 'stats', 'config', 'capabilities', 'emit']).describe('Debug action to perform'),
37
+ root: z.string().optional().describe('Workspace root path (optional)'),
38
+ type: z.enum(['logging', 'elicitation']).optional().describe('For emit: type of notification to send'),
39
+ message: z.string().optional().describe('Message for emit action'),
40
+ };
41
+ /** Handle debug tool call */
42
+ export async function handleDebugTool(input) {
43
+ // Validate input
44
+ const parsed = debugInputSchema.safeParse(input);
45
+ if (!parsed.success) {
46
+ throw new Error(`Invalid input: ${parsed.error.message}`);
47
+ }
48
+ const { action, root, type, message } = parsed.data;
49
+ logger.debug(`Debug action: ${action}, root: ${root ?? 'all'}`);
50
+ switch (action) {
51
+ case 'state': {
52
+ const workspaces = root
53
+ ? [workspaceManager.getWorkspace(root)].filter(Boolean)
54
+ : workspaceManager.getAllWorkspaces();
55
+ const state = {
56
+ workspaceCount: workspaceManager.count,
57
+ workspaces: workspaces.map((ws) => {
58
+ if (!ws)
59
+ return null;
60
+ const status = ws.getStatus();
61
+ return {
62
+ root: ws.root,
63
+ status: status.status,
64
+ indexed: status.indexed,
65
+ pending: status.pending,
66
+ watching: status.watching,
67
+ lastIndexed: status.lastIndexed,
68
+ error: status.error,
69
+ progress: status.progress,
70
+ };
71
+ }).filter(Boolean),
72
+ };
73
+ return JSON.stringify(state, null, 2);
74
+ }
75
+ case 'stats': {
76
+ const workspaces = root
77
+ ? [workspaceManager.getWorkspace(root)].filter(Boolean)
78
+ : workspaceManager.getAllWorkspaces();
79
+ const stats = {
80
+ totalWorkspaces: workspaceManager.count,
81
+ workspaces: workspaces.map((ws) => {
82
+ if (!ws)
83
+ return null;
84
+ const status = ws.getStatus();
85
+ const paths = ws.getIndexedPaths();
86
+ return {
87
+ root: ws.root,
88
+ indexedFiles: paths.length,
89
+ status: status.status,
90
+ pending: status.pending,
91
+ };
92
+ }).filter(Boolean),
93
+ totals: {
94
+ indexedFiles: workspaces.reduce((sum, ws) => sum + (ws?.getIndexedPaths().length ?? 0), 0),
95
+ pending: workspaces.reduce((sum, ws) => sum + (ws?.getStatus().pending ?? 0), 0),
96
+ },
97
+ };
98
+ return JSON.stringify(stats, null, 2);
99
+ }
100
+ case 'config': {
101
+ const config = {
102
+ indexing: INDEXING_CONFIG,
103
+ watcher: WATCHER_CONFIG,
104
+ persistence: {
105
+ ...PERSISTENCE_CONFIG,
106
+ enabled: isPersistenceEnabled(),
107
+ },
108
+ environment: {
109
+ SUCO_AUGGIE_DEBUG: process.env.SUCO_AUGGIE_DEBUG ?? '(not set)',
110
+ SUCO_AUGGIE_DEBOUNCE_MS: process.env.SUCO_AUGGIE_DEBOUNCE_MS ?? '(not set)',
111
+ AUGMENT_API_TOKEN: process.env.AUGMENT_API_TOKEN ? '(set)' : '(not set)',
112
+ AUGMENT_API_URL: process.env.AUGMENT_API_URL ?? '(not set)',
113
+ },
114
+ };
115
+ return JSON.stringify(config, null, 2);
116
+ }
117
+ case 'capabilities': {
118
+ const server = getMcpServer();
119
+ const clientCaps = server?.server.getClientCapabilities();
120
+ const caps = {
121
+ serverConnected: !!server,
122
+ clientCapabilities: clientCaps ?? null,
123
+ features: {
124
+ elicitation: clientSupportsElicitation(),
125
+ logging: true, // We always try to send logs
126
+ },
127
+ };
128
+ return JSON.stringify(caps, null, 2);
129
+ }
130
+ case 'emit': {
131
+ const emitType = type ?? 'logging';
132
+ const msg = message ?? 'Test message from debug tool';
133
+ if (emitType === 'logging') {
134
+ await sendLogToClient('info', msg);
135
+ return JSON.stringify({
136
+ type: 'logging',
137
+ sent: true,
138
+ message: msg,
139
+ }, null, 2);
140
+ }
141
+ if (emitType === 'elicitation') {
142
+ if (!clientSupportsElicitation()) {
143
+ return JSON.stringify({
144
+ type: 'elicitation',
145
+ supported: false,
146
+ error: 'Client does not support elicitation',
147
+ }, null, 2);
148
+ }
149
+ const result = await elicitInput({
150
+ message: msg,
151
+ requestedSchema: {
152
+ type: 'object',
153
+ properties: {
154
+ response: {
155
+ type: 'string',
156
+ title: 'Your Response',
157
+ description: 'Enter any text to test elicitation',
158
+ },
159
+ confirmed: {
160
+ type: 'boolean',
161
+ title: 'Confirm',
162
+ description: 'Check to confirm',
163
+ },
164
+ },
165
+ required: ['response'],
166
+ },
167
+ });
168
+ return JSON.stringify({
169
+ type: 'elicitation',
170
+ supported: true,
171
+ result: result ?? { error: 'Elicitation returned null' },
172
+ }, null, 2);
173
+ }
174
+ throw new Error(`Unknown emit type: ${emitType}`);
175
+ }
176
+ default:
177
+ throw new Error(`Unknown action: ${action}`);
178
+ }
179
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Index tool - manage and inspect indexing state
3
+ */
4
+ import { z } from 'zod';
5
+ /** Tool name */
6
+ export declare const INDEX_TOOL_NAME = "index";
7
+ /** Tool description */
8
+ export declare const INDEX_TOOL_DESCRIPTION = "Manage and inspect codebase indexing state.\n\nActions:\n- status: Get indexing status for all workspaces\n- list: List top-level indexed directories with file counts\n- reindex: Force full re-index of all workspaces (clears and rebuilds)\n\nParameters:\n- action: \"status\", \"list\", or \"reindex\" (required)";
9
+ /** Input schema for zod validation */
10
+ export declare const indexInputSchema: z.ZodObject<{
11
+ action: z.ZodEnum<["status", "list", "reindex"]>;
12
+ }, "strip", z.ZodTypeAny, {
13
+ action: "status" | "list" | "reindex";
14
+ }, {
15
+ action: "status" | "list" | "reindex";
16
+ }>;
17
+ /** Zod schema shape for MCP tool registration */
18
+ export declare const INDEX_PARAMS_SHAPE: {
19
+ action: z.ZodEnum<["status", "list", "reindex"]>;
20
+ };
21
+ /** Handle index tool call */
22
+ export declare function handleIndexTool(input: unknown): Promise<string>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Index tool - manage and inspect indexing state
3
+ */
4
+ import { z } from 'zod';
5
+ import { workspaceManager } from '../workspace-manager.js';
6
+ import { createLogger } from '../logger.js';
7
+ const logger = createLogger('IndexTool');
8
+ /** Tool name */
9
+ export const INDEX_TOOL_NAME = 'index';
10
+ /** Tool description */
11
+ export const INDEX_TOOL_DESCRIPTION = `Manage and inspect codebase indexing state.
12
+
13
+ Actions:
14
+ - status: Get indexing status for all workspaces
15
+ - list: List top-level indexed directories with file counts
16
+ - reindex: Force full re-index of all workspaces (clears and rebuilds)
17
+
18
+ Parameters:
19
+ - action: "status", "list", or "reindex" (required)`;
20
+ /** Input schema for zod validation */
21
+ export const indexInputSchema = z.object({
22
+ action: z.enum(['status', 'list', 'reindex']),
23
+ });
24
+ /** Zod schema shape for MCP tool registration */
25
+ export const INDEX_PARAMS_SHAPE = {
26
+ action: z.enum(['status', 'list', 'reindex']).describe('Action to perform'),
27
+ };
28
+ /** Handle index tool call */
29
+ export async function handleIndexTool(input) {
30
+ // Validate input
31
+ const parsed = indexInputSchema.safeParse(input);
32
+ if (!parsed.success) {
33
+ throw new Error(`Invalid input: ${parsed.error.message}`);
34
+ }
35
+ const { action } = parsed.data;
36
+ logger.debug(`Index action: ${action}`);
37
+ switch (action) {
38
+ case 'status': {
39
+ const statuses = workspaceManager.getStatus();
40
+ if (statuses.length === 0) {
41
+ return JSON.stringify({ workspaces: [], message: 'No workspaces configured' });
42
+ }
43
+ return JSON.stringify({ workspaces: statuses }, null, 2);
44
+ }
45
+ case 'list': {
46
+ const paths = workspaceManager.getIndexedPaths();
47
+ // Group by top-level directory
48
+ const dirCounts = {};
49
+ for (const filePath of paths) {
50
+ const parts = filePath.split('/');
51
+ // Get first directory component (or file if at root)
52
+ const topLevel = parts.length > 1 ? parts[0] : '(root files)';
53
+ dirCounts[topLevel] = (dirCounts[topLevel] ?? 0) + 1;
54
+ }
55
+ // Sort by count descending
56
+ const directories = Object.entries(dirCounts)
57
+ .sort((a, b) => b[1] - a[1])
58
+ .map(([dir, count]) => ({ directory: dir, files: count }));
59
+ return JSON.stringify({
60
+ totalFiles: paths.length,
61
+ directories,
62
+ }, null, 2);
63
+ }
64
+ case 'reindex': {
65
+ const workspaces = workspaceManager.getAllWorkspaces();
66
+ const results = [];
67
+ for (const workspace of workspaces) {
68
+ // Trigger reindex without waiting - runs in background
69
+ workspace.reindex(true).catch(err => {
70
+ const msg = err instanceof Error ? err.message : String(err);
71
+ logger.error(`Reindex failed for ${workspace.root}: ${msg}`);
72
+ });
73
+ results.push({
74
+ root: workspace.root,
75
+ status: 'indexing',
76
+ });
77
+ }
78
+ return JSON.stringify({
79
+ message: `Full reindex triggered for ${workspaces.length} workspace(s). Use 'status' action to monitor progress.`,
80
+ workspaces: results,
81
+ }, null, 2);
82
+ }
83
+ default:
84
+ throw new Error(`Unknown action: ${action}`);
85
+ }
86
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Type definitions for suco-auggie-mcp
3
+ */
4
+ /** Workspace indexing status */
5
+ export type WorkspaceStatusType = 'initializing' | 'indexing' | 'ready' | 'error';
6
+ /** Progress tracking for indexing operations */
7
+ export interface IndexingProgress {
8
+ /** Files discovered so far (grows during scan) */
9
+ discovered: number;
10
+ /** Files indexed so far */
11
+ indexed: number;
12
+ /** Whether discovery is still in progress */
13
+ discovering: boolean;
14
+ /** Batches queued for indexing */
15
+ batchesQueued: number;
16
+ /** Batches completed (success or permanent failure) */
17
+ batchesCompleted: number;
18
+ /** Batches that failed and are pending retry */
19
+ batchesFailed: number;
20
+ /** Batches successfully retried */
21
+ batchesRetried: number;
22
+ }
23
+ /** Error classification for retry logic */
24
+ export type BatchErrorType = 'retryable' | 'fatal' | 'permanent';
25
+ /** Failed batch awaiting retry */
26
+ export interface FailedBatch {
27
+ /** Batch number for logging */
28
+ batchNum: number;
29
+ /** Files in the batch */
30
+ files: Array<{
31
+ path: string;
32
+ contents: string;
33
+ }>;
34
+ /** Total bytes in the batch */
35
+ bytes: number;
36
+ /** Number of retry attempts so far */
37
+ attempts: number;
38
+ /** Error type classification */
39
+ errorType: BatchErrorType;
40
+ /** Last error message */
41
+ lastError: string;
42
+ /** Timestamp of last attempt */
43
+ lastAttemptAt: Date;
44
+ /** Next retry scheduled at (null if not scheduled) */
45
+ nextRetryAt: Date | null;
46
+ }
47
+ /** Retry statistics for status reporting */
48
+ export interface RetryStats {
49
+ /** Batches currently pending immediate retry */
50
+ pendingRetries: number;
51
+ /** Total files in pending immediate retry batches */
52
+ pendingRetryFiles: number;
53
+ /** Batches that were successfully retried (immediate) */
54
+ retriedBatches: number;
55
+ /** Files that were successfully retried (immediate) */
56
+ retriedFiles: number;
57
+ /** Batches that permanently failed (exhausted all retries including cooldown) */
58
+ permanentlyFailedBatches: number;
59
+ /** Files that permanently failed */
60
+ permanentlyFailedFiles: number;
61
+ /** Cooldown queue stats */
62
+ cooldown: CooldownStats;
63
+ }
64
+ /** Cooldown queue statistics */
65
+ export interface CooldownStats {
66
+ /** Files currently in cooldown queue */
67
+ pendingFiles: number;
68
+ /** Current cooldown cycle (0 if not in cooldown) */
69
+ currentCycle: number;
70
+ /** Maximum cooldown cycles allowed */
71
+ maxCycles: number;
72
+ /** Next cooldown retry time (ISO string, null if not scheduled) */
73
+ nextRetryAt: string | null;
74
+ /** Files recovered via cooldown */
75
+ recoveredFiles: number;
76
+ }
77
+ /** Entry in the cooldown queue (persisted) - stores paths only, re-reads contents on retry */
78
+ export interface CooldownEntry {
79
+ /** Relative file path */
80
+ path: string;
81
+ /** Cooldown cycle count (how many cooldown periods completed) */
82
+ cooldownCycle: number;
83
+ /** Last error message */
84
+ lastError: string;
85
+ /** Timestamp when added to cooldown (ISO string) */
86
+ addedAt: string;
87
+ /** Timestamp of last cooldown retry attempt (ISO string, null if not yet retried) */
88
+ lastRetryAt: string | null;
89
+ }
90
+ /** Persisted cooldown queue structure */
91
+ export interface PersistedCooldownQueue {
92
+ /** Version for future schema migrations */
93
+ version: 1;
94
+ /** Timestamp when queue was last saved */
95
+ savedAt: string;
96
+ /** Next scheduled cooldown retry (ISO string, null if not scheduled) */
97
+ nextRetryAt: string | null;
98
+ /** Cooldown entries */
99
+ entries: CooldownEntry[];
100
+ }
101
+ /** Drift reconciliation statistics */
102
+ export interface DriftStats {
103
+ /** Files that were deleted while MCP was offline */
104
+ deletedFiles: number;
105
+ /** Files that were modified while MCP was offline */
106
+ modifiedFiles: number;
107
+ /** Whether drift reconciliation was performed */
108
+ reconciled: boolean;
109
+ /** Time taken for reconciliation in ms */
110
+ reconcileTimeMs: number | null;
111
+ }
112
+ /** Stored metadata for a single file (for mtime-based change detection) */
113
+ export interface FileMetadataEntry {
114
+ /** File modification time in milliseconds since epoch */
115
+ mtimeMs: number;
116
+ /** File size in bytes */
117
+ size: number;
118
+ }
119
+ /** Map of relative paths to their metadata */
120
+ export type FileMetadataMap = Record<string, FileMetadataEntry>;
121
+ /** Persisted metadata file structure */
122
+ export interface PersistedMetadata {
123
+ /** Version for future schema migrations */
124
+ version: 1;
125
+ /** Timestamp when metadata was last saved */
126
+ savedAt: string;
127
+ /** File metadata entries */
128
+ files: FileMetadataMap;
129
+ }
130
+ /** Status of a single workspace */
131
+ export interface WorkspaceStatus {
132
+ /** Absolute path to workspace root */
133
+ root: string;
134
+ /** Current status */
135
+ status: WorkspaceStatusType;
136
+ /** Total files processed (indexed + skipped + failed + pending) */
137
+ total: number;
138
+ /** Files skipped (too large, binary, unreadable, etc.) */
139
+ skipped: number;
140
+ /** Files that permanently failed indexing */
141
+ failed: number;
142
+ /** Number of pending files (change queue + retry queue) */
143
+ pending: number;
144
+ /** Number of indexed files */
145
+ indexed: number;
146
+ /** Whether file watcher is active */
147
+ watching: boolean;
148
+ /** Last indexing timestamp (ISO string) */
149
+ lastIndexed: string | null;
150
+ /** Error message if status is 'error' */
151
+ error?: string;
152
+ /** Indexing progress if status is 'indexing' */
153
+ progress?: IndexingProgress;
154
+ /** Timing information */
155
+ timing?: {
156
+ /** ISO timestamp when current indexing started (null if not indexing) */
157
+ startedAt: string | null;
158
+ /** Elapsed seconds since indexing started (live during indexing) */
159
+ elapsedSeconds: number | null;
160
+ /** Initial full index time in seconds (null if not yet completed) */
161
+ initialIndexSeconds: number | null;
162
+ /** Last incremental/delta index time in seconds (null if no deltas yet) */
163
+ lastDeltaSeconds: number | null;
164
+ };
165
+ /** Retry statistics */
166
+ retry?: RetryStats;
167
+ /** Concurrency metrics */
168
+ concurrency?: {
169
+ /** Current concurrency level */
170
+ current: number;
171
+ /** Peak concurrency reached */
172
+ peak: number;
173
+ /** Min/max concurrency bounds */
174
+ bounds: {
175
+ min: number;
176
+ max: number;
177
+ };
178
+ /** Batch size limits */
179
+ batchLimits: {
180
+ maxBytes: number;
181
+ maxFiles: number;
182
+ };
183
+ };
184
+ /** Drift reconciliation stats (only when persistence enabled) */
185
+ drift?: DriftStats;
186
+ }
187
+ /** Codebase tool input - search mode */
188
+ export interface CodebaseSearchInput {
189
+ mode: 'search';
190
+ query: string;
191
+ root?: string;
192
+ }
193
+ /** Codebase tool input - ask mode */
194
+ export interface CodebaseAskInput {
195
+ mode: 'ask';
196
+ query: string;
197
+ prompt?: string;
198
+ root?: string;
199
+ }
200
+ /** Codebase tool input union */
201
+ export type CodebaseInput = CodebaseSearchInput | CodebaseAskInput;
202
+ /** Index tool input - status action */
203
+ export interface IndexStatusInput {
204
+ action: 'status';
205
+ root?: string;
206
+ }
207
+ /** Index tool input - list action */
208
+ export interface IndexListInput {
209
+ action: 'list';
210
+ root?: string;
211
+ limit?: number;
212
+ }
213
+ /** Index tool input - reindex action */
214
+ export interface IndexReindexInput {
215
+ action: 'reindex';
216
+ root?: string;
217
+ full?: boolean;
218
+ }
219
+ /** Index tool input union */
220
+ export type IndexInput = IndexStatusInput | IndexListInput | IndexReindexInput;
221
+ /** File entry for indexing */
222
+ export interface FileEntry {
223
+ /** Relative path from workspace root */
224
+ path: string;
225
+ /** File contents */
226
+ contents: string;
227
+ }
228
+ /** File change event */
229
+ export interface FileChange {
230
+ type: 'add' | 'change' | 'unlink';
231
+ path: string;
232
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Type definitions for suco-auggie-mcp
3
+ */
4
+ export {};
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Multi-workspace coordinator
3
+ */
4
+ import { Workspace } from './workspace.js';
5
+ import type { WorkspaceStatus } from './types.js';
6
+ /** Manages multiple workspaces */
7
+ export declare class WorkspaceManager {
8
+ private workspaces;
9
+ private defaultRoot;
10
+ /** Add a workspace by path */
11
+ addWorkspace(rootPath: string): Promise<Workspace>;
12
+ /** Add multiple workspaces, deduplicating by resolved path */
13
+ addWorkspaces(rootPaths: string[]): Promise<void>;
14
+ /** Get a workspace by root path, or default if not specified */
15
+ getWorkspace(root?: string): Workspace | null;
16
+ /** Resolve root parameter to a workspace, throwing if not found */
17
+ resolveWorkspace(root?: string): Workspace;
18
+ /** Get all workspaces */
19
+ getAllWorkspaces(): Workspace[];
20
+ /** Get status for all workspaces */
21
+ getStatus(): WorkspaceStatus[];
22
+ /** Get indexed paths from all workspaces (relative to workspace root) */
23
+ getIndexedPaths(): string[];
24
+ /** Check if any workspaces are configured */
25
+ hasWorkspaces(): boolean;
26
+ /** Get count of workspaces */
27
+ get count(): number;
28
+ /** Close all workspaces */
29
+ close(): Promise<void>;
30
+ /** Re-initialize all workspaces in error state (called when auth becomes available) */
31
+ reinitializeErrored(): Promise<void>;
32
+ }
33
+ /** Singleton instance */
34
+ export declare const workspaceManager: WorkspaceManager;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Multi-workspace coordinator
3
+ */
4
+ import * as path from 'node:path';
5
+ import { Workspace } from './workspace.js';
6
+ import { createLogger } from './logger.js';
7
+ const logger = createLogger('WorkspaceManager');
8
+ /** Manages multiple workspaces */
9
+ export class WorkspaceManager {
10
+ workspaces = new Map();
11
+ defaultRoot = null;
12
+ /** Add a workspace by path */
13
+ async addWorkspace(rootPath) {
14
+ const resolved = path.resolve(rootPath);
15
+ // Check if already exists
16
+ if (this.workspaces.has(resolved)) {
17
+ logger.debug(`Workspace already exists: ${resolved}`);
18
+ return this.workspaces.get(resolved);
19
+ }
20
+ logger.info(`Adding workspace: ${resolved}`);
21
+ const workspace = await Workspace.create(resolved);
22
+ this.workspaces.set(resolved, workspace);
23
+ // Set first workspace as default
24
+ if (this.defaultRoot === null) {
25
+ this.defaultRoot = resolved;
26
+ }
27
+ return workspace;
28
+ }
29
+ /** Add multiple workspaces, deduplicating by resolved path */
30
+ async addWorkspaces(rootPaths) {
31
+ const uniquePaths = [...new Set(rootPaths.map((p) => path.resolve(p)))];
32
+ for (const rootPath of uniquePaths) {
33
+ await this.addWorkspace(rootPath);
34
+ }
35
+ }
36
+ /** Get a workspace by root path, or default if not specified */
37
+ getWorkspace(root) {
38
+ if (root) {
39
+ const resolved = path.resolve(root);
40
+ return this.workspaces.get(resolved) ?? null;
41
+ }
42
+ if (this.defaultRoot) {
43
+ return this.workspaces.get(this.defaultRoot) ?? null;
44
+ }
45
+ return null;
46
+ }
47
+ /** Resolve root parameter to a workspace, throwing if not found */
48
+ resolveWorkspace(root) {
49
+ const workspace = this.getWorkspace(root);
50
+ if (!workspace) {
51
+ if (root) {
52
+ throw new Error(`Workspace not found: ${root}`);
53
+ }
54
+ throw new Error('No workspaces configured');
55
+ }
56
+ return workspace;
57
+ }
58
+ /** Get all workspaces */
59
+ getAllWorkspaces() {
60
+ return [...this.workspaces.values()];
61
+ }
62
+ /** Get status for all workspaces */
63
+ getStatus() {
64
+ return this.getAllWorkspaces().map((w) => w.getStatus());
65
+ }
66
+ /** Get indexed paths from all workspaces (relative to workspace root) */
67
+ getIndexedPaths() {
68
+ let paths = [];
69
+ for (const workspace of this.getAllWorkspaces()) {
70
+ paths = paths.concat(workspace.getIndexedPaths());
71
+ }
72
+ return paths;
73
+ }
74
+ /** Check if any workspaces are configured */
75
+ hasWorkspaces() {
76
+ return this.workspaces.size > 0;
77
+ }
78
+ /** Get count of workspaces */
79
+ get count() {
80
+ return this.workspaces.size;
81
+ }
82
+ /** Close all workspaces */
83
+ async close() {
84
+ const closePromises = [...this.workspaces.values()].map((w) => w.close());
85
+ await Promise.all(closePromises);
86
+ this.workspaces.clear();
87
+ this.defaultRoot = null;
88
+ logger.info('All workspaces closed');
89
+ }
90
+ /** Re-initialize all workspaces in error state (called when auth becomes available) */
91
+ async reinitializeErrored() {
92
+ const erroredWorkspaces = this.getAllWorkspaces().filter((w) => w.getStatus().status === 'error');
93
+ if (erroredWorkspaces.length === 0) {
94
+ logger.debug('No errored workspaces to reinitialize');
95
+ return;
96
+ }
97
+ logger.info(`Re-initializing ${erroredWorkspaces.length} errored workspace(s)`);
98
+ for (const workspace of erroredWorkspaces) {
99
+ await workspace.reinitialize();
100
+ }
101
+ }
102
+ }
103
+ /** Singleton instance */
104
+ export const workspaceManager = new WorkspaceManager();