@stackmemoryai/stackmemory 0.5.21 → 0.5.23

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,128 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import Anthropic from "@anthropic-ai/sdk";
6
+ import { logger } from "../monitoring/logger.js";
7
+ class AnthropicLLMProvider {
8
+ client;
9
+ model;
10
+ temperature;
11
+ maxRetries;
12
+ timeout;
13
+ constructor(config) {
14
+ this.client = new Anthropic({
15
+ apiKey: config.apiKey
16
+ });
17
+ this.model = config.model || "claude-3-haiku-20240307";
18
+ this.temperature = config.temperature ?? 0.3;
19
+ this.maxRetries = config.maxRetries ?? 2;
20
+ this.timeout = config.timeout ?? 3e4;
21
+ logger.info("AnthropicLLMProvider initialized", {
22
+ model: this.model,
23
+ temperature: this.temperature
24
+ });
25
+ }
26
+ /**
27
+ * Analyze a prompt using the Anthropic API
28
+ */
29
+ async analyze(prompt, maxTokens) {
30
+ const startTime = Date.now();
31
+ let lastError = null;
32
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
33
+ try {
34
+ const response = await this.makeRequest(prompt, maxTokens);
35
+ logger.debug("LLM analysis completed", {
36
+ model: this.model,
37
+ promptLength: prompt.length,
38
+ responseLength: response.length,
39
+ durationMs: Date.now() - startTime,
40
+ attempt
41
+ });
42
+ return response;
43
+ } catch (error) {
44
+ lastError = error instanceof Error ? error : new Error(String(error));
45
+ if (this.isRetryableError(error) && attempt < this.maxRetries) {
46
+ const backoffMs = Math.pow(2, attempt) * 1e3;
47
+ logger.warn("LLM request failed, retrying", {
48
+ attempt,
49
+ backoffMs,
50
+ error: lastError.message
51
+ });
52
+ await this.sleep(backoffMs);
53
+ continue;
54
+ }
55
+ break;
56
+ }
57
+ }
58
+ logger.error("LLM analysis failed after retries", lastError);
59
+ throw lastError;
60
+ }
61
+ /**
62
+ * Make the actual API request
63
+ */
64
+ async makeRequest(prompt, maxTokens) {
65
+ const controller = new AbortController();
66
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
67
+ try {
68
+ const response = await this.client.messages.create({
69
+ model: this.model,
70
+ max_tokens: maxTokens,
71
+ temperature: this.temperature,
72
+ messages: [
73
+ {
74
+ role: "user",
75
+ content: prompt
76
+ }
77
+ ]
78
+ });
79
+ const textContent = response.content.find((c) => c.type === "text");
80
+ if (!textContent || textContent.type !== "text") {
81
+ throw new Error("No text content in response");
82
+ }
83
+ return textContent.text;
84
+ } finally {
85
+ clearTimeout(timeoutId);
86
+ }
87
+ }
88
+ /**
89
+ * Check if an error is retryable
90
+ */
91
+ isRetryableError(error) {
92
+ if (error instanceof Anthropic.RateLimitError) {
93
+ return true;
94
+ }
95
+ if (error instanceof Anthropic.APIConnectionError) {
96
+ return true;
97
+ }
98
+ if (error instanceof Anthropic.InternalServerError) {
99
+ return true;
100
+ }
101
+ if (error instanceof Error && error.name === "AbortError") {
102
+ return true;
103
+ }
104
+ return false;
105
+ }
106
+ sleep(ms) {
107
+ return new Promise((resolve) => setTimeout(resolve, ms));
108
+ }
109
+ }
110
+ function createLLMProvider() {
111
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
112
+ if (!apiKey) {
113
+ logger.info(
114
+ "No ANTHROPIC_API_KEY found, LLM retrieval will use heuristics"
115
+ );
116
+ return void 0;
117
+ }
118
+ return new AnthropicLLMProvider({
119
+ apiKey,
120
+ model: process.env["ANTHROPIC_MODEL"] || "claude-3-haiku-20240307",
121
+ temperature: parseFloat(process.env["ANTHROPIC_TEMPERATURE"] || "0.3")
122
+ });
123
+ }
124
+ export {
125
+ AnthropicLLMProvider,
126
+ createLLMProvider
127
+ };
128
+ //# sourceMappingURL=llm-provider.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/core/retrieval/llm-provider.ts"],
4
+ "sourcesContent": ["/**\n * LLM Provider Implementation for Context Retrieval\n * Provides real Anthropic API integration for intelligent context analysis\n */\n\nimport Anthropic from '@anthropic-ai/sdk';\nimport { logger } from '../monitoring/logger.js';\n\n/**\n * LLM provider interface for context analysis\n */\nexport interface LLMProvider {\n analyze(prompt: string, maxTokens: number): Promise<string>;\n}\n\n/**\n * Configuration for Anthropic LLM provider\n */\nexport interface AnthropicProviderConfig {\n apiKey: string;\n model?: string;\n temperature?: number;\n maxRetries?: number;\n timeout?: number;\n}\n\n/**\n * Real Anthropic LLM provider using the official SDK\n */\nexport class AnthropicLLMProvider implements LLMProvider {\n private client: Anthropic;\n private model: string;\n private temperature: number;\n private maxRetries: number;\n private timeout: number;\n\n constructor(config: AnthropicProviderConfig) {\n this.client = new Anthropic({\n apiKey: config.apiKey,\n });\n this.model = config.model || 'claude-3-haiku-20240307';\n this.temperature = config.temperature ?? 0.3;\n this.maxRetries = config.maxRetries ?? 2;\n this.timeout = config.timeout ?? 30000;\n\n logger.info('AnthropicLLMProvider initialized', {\n model: this.model,\n temperature: this.temperature,\n });\n }\n\n /**\n * Analyze a prompt using the Anthropic API\n */\n async analyze(prompt: string, maxTokens: number): Promise<string> {\n const startTime = Date.now();\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n try {\n const response = await this.makeRequest(prompt, maxTokens);\n\n logger.debug('LLM analysis completed', {\n model: this.model,\n promptLength: prompt.length,\n responseLength: response.length,\n durationMs: Date.now() - startTime,\n attempt,\n });\n\n return response;\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n\n // Check if retryable\n if (this.isRetryableError(error) && attempt < this.maxRetries) {\n const backoffMs = Math.pow(2, attempt) * 1000;\n logger.warn('LLM request failed, retrying', {\n attempt,\n backoffMs,\n error: lastError.message,\n });\n await this.sleep(backoffMs);\n continue;\n }\n\n break;\n }\n }\n\n logger.error('LLM analysis failed after retries', lastError!);\n throw lastError;\n }\n\n /**\n * Make the actual API request\n */\n private async makeRequest(\n prompt: string,\n maxTokens: number\n ): Promise<string> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await this.client.messages.create({\n model: this.model,\n max_tokens: maxTokens,\n temperature: this.temperature,\n messages: [\n {\n role: 'user',\n content: prompt,\n },\n ],\n });\n\n // Extract text from response\n const textContent = response.content.find((c) => c.type === 'text');\n if (!textContent || textContent.type !== 'text') {\n throw new Error('No text content in response');\n }\n\n return textContent.text;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Check if an error is retryable\n */\n private isRetryableError(error: unknown): boolean {\n if (error instanceof Anthropic.RateLimitError) {\n return true;\n }\n if (error instanceof Anthropic.APIConnectionError) {\n return true;\n }\n if (error instanceof Anthropic.InternalServerError) {\n return true;\n }\n // Timeout errors are retryable\n if (error instanceof Error && error.name === 'AbortError') {\n return true;\n }\n return false;\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\n/**\n * Factory function to create an LLM provider based on environment\n */\nexport function createLLMProvider(): LLMProvider | undefined {\n const apiKey = process.env['ANTHROPIC_API_KEY'];\n\n if (!apiKey) {\n logger.info(\n 'No ANTHROPIC_API_KEY found, LLM retrieval will use heuristics'\n );\n return undefined;\n }\n\n return new AnthropicLLMProvider({\n apiKey,\n model: process.env['ANTHROPIC_MODEL'] || 'claude-3-haiku-20240307',\n temperature: parseFloat(process.env['ANTHROPIC_TEMPERATURE'] || '0.3'),\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,236 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { v4 as uuidv4 } from "uuid";
6
+ import { logger } from "../monitoring/logger.js";
7
+ class RetrievalAuditStore {
8
+ db;
9
+ projectId;
10
+ initialized = false;
11
+ constructor(db, projectId) {
12
+ this.db = db;
13
+ this.projectId = projectId;
14
+ this.initSchema();
15
+ }
16
+ /**
17
+ * Initialize the audit table schema
18
+ */
19
+ initSchema() {
20
+ if (this.initialized) return;
21
+ try {
22
+ this.db.exec(`
23
+ CREATE TABLE IF NOT EXISTS retrieval_audit (
24
+ id TEXT PRIMARY KEY,
25
+ timestamp INTEGER NOT NULL,
26
+ project_id TEXT NOT NULL,
27
+ query TEXT NOT NULL,
28
+ reasoning TEXT NOT NULL,
29
+ frames_retrieved TEXT NOT NULL,
30
+ confidence_score REAL NOT NULL,
31
+ provider TEXT NOT NULL,
32
+ tokens_used INTEGER NOT NULL,
33
+ token_budget INTEGER NOT NULL,
34
+ analysis_time_ms INTEGER NOT NULL,
35
+ query_complexity TEXT NOT NULL
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_retrieval_audit_project_time
39
+ ON retrieval_audit(project_id, timestamp DESC);
40
+
41
+ CREATE INDEX IF NOT EXISTS idx_retrieval_audit_query
42
+ ON retrieval_audit(project_id, query);
43
+ `);
44
+ this.initialized = true;
45
+ logger.debug("Retrieval audit schema initialized");
46
+ } catch (error) {
47
+ logger.warn("Failed to initialize retrieval audit schema", {
48
+ error: error instanceof Error ? error.message : String(error)
49
+ });
50
+ }
51
+ }
52
+ /**
53
+ * Record a retrieval decision
54
+ */
55
+ record(query, analysis, options) {
56
+ const id = uuidv4();
57
+ const timestamp = Date.now();
58
+ try {
59
+ const stmt = this.db.prepare(`
60
+ INSERT INTO retrieval_audit (
61
+ id, timestamp, project_id, query, reasoning, frames_retrieved,
62
+ confidence_score, provider, tokens_used, token_budget,
63
+ analysis_time_ms, query_complexity
64
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
65
+ `);
66
+ stmt.run(
67
+ id,
68
+ timestamp,
69
+ this.projectId,
70
+ query,
71
+ analysis.reasoning,
72
+ JSON.stringify(analysis.framesToRetrieve.map((f) => f.frameId)),
73
+ analysis.confidenceScore,
74
+ options.provider,
75
+ options.tokensUsed,
76
+ options.tokenBudget,
77
+ analysis.metadata.analysisTimeMs,
78
+ analysis.metadata.queryComplexity
79
+ );
80
+ logger.debug("Recorded retrieval audit entry", {
81
+ id,
82
+ query: query.slice(0, 50)
83
+ });
84
+ return id;
85
+ } catch (error) {
86
+ logger.warn("Failed to record retrieval audit", {
87
+ error: error instanceof Error ? error.message : String(error)
88
+ });
89
+ return id;
90
+ }
91
+ }
92
+ /**
93
+ * Get recent retrieval audit entries
94
+ */
95
+ getRecent(limit = 10) {
96
+ try {
97
+ const stmt = this.db.prepare(`
98
+ SELECT * FROM retrieval_audit
99
+ WHERE project_id = ?
100
+ ORDER BY timestamp DESC
101
+ LIMIT ?
102
+ `);
103
+ const rows = stmt.all(this.projectId, limit);
104
+ return rows.map(this.rowToEntry);
105
+ } catch (error) {
106
+ logger.warn("Failed to get recent audit entries", {
107
+ error: error instanceof Error ? error.message : String(error)
108
+ });
109
+ return [];
110
+ }
111
+ }
112
+ /**
113
+ * Get audit entry by ID
114
+ */
115
+ getById(id) {
116
+ try {
117
+ const stmt = this.db.prepare(`
118
+ SELECT * FROM retrieval_audit WHERE id = ?
119
+ `);
120
+ const row = stmt.get(id);
121
+ return row ? this.rowToEntry(row) : null;
122
+ } catch (error) {
123
+ logger.warn("Failed to get audit entry", {
124
+ error: error instanceof Error ? error.message : String(error),
125
+ id
126
+ });
127
+ return null;
128
+ }
129
+ }
130
+ /**
131
+ * Search audit entries by query text
132
+ */
133
+ searchByQuery(searchTerm, limit = 10) {
134
+ try {
135
+ const stmt = this.db.prepare(`
136
+ SELECT * FROM retrieval_audit
137
+ WHERE project_id = ? AND query LIKE ?
138
+ ORDER BY timestamp DESC
139
+ LIMIT ?
140
+ `);
141
+ const rows = stmt.all(this.projectId, `%${searchTerm}%`, limit);
142
+ return rows.map(this.rowToEntry);
143
+ } catch (error) {
144
+ logger.warn("Failed to search audit entries", {
145
+ error: error instanceof Error ? error.message : String(error)
146
+ });
147
+ return [];
148
+ }
149
+ }
150
+ /**
151
+ * Get statistics about retrieval patterns
152
+ */
153
+ getStats() {
154
+ try {
155
+ const statsStmt = this.db.prepare(`
156
+ SELECT
157
+ COUNT(*) as total,
158
+ AVG(confidence_score) as avg_confidence,
159
+ AVG(tokens_used) as avg_tokens,
160
+ AVG(analysis_time_ms) as avg_time
161
+ FROM retrieval_audit
162
+ WHERE project_id = ?
163
+ `);
164
+ const providerStmt = this.db.prepare(`
165
+ SELECT provider, COUNT(*) as count
166
+ FROM retrieval_audit
167
+ WHERE project_id = ?
168
+ GROUP BY provider
169
+ `);
170
+ const stats = statsStmt.get(this.projectId);
171
+ const providers = providerStmt.all(this.projectId);
172
+ const providerBreakdown = {};
173
+ for (const p of providers) {
174
+ providerBreakdown[p.provider] = p.count;
175
+ }
176
+ return {
177
+ totalRetrievals: stats?.total || 0,
178
+ avgConfidence: stats?.avg_confidence || 0,
179
+ providerBreakdown,
180
+ avgTokensUsed: stats?.avg_tokens || 0,
181
+ avgAnalysisTime: stats?.avg_time || 0
182
+ };
183
+ } catch (error) {
184
+ logger.warn("Failed to get audit stats", {
185
+ error: error instanceof Error ? error.message : String(error)
186
+ });
187
+ return {
188
+ totalRetrievals: 0,
189
+ avgConfidence: 0,
190
+ providerBreakdown: {},
191
+ avgTokensUsed: 0,
192
+ avgAnalysisTime: 0
193
+ };
194
+ }
195
+ }
196
+ /**
197
+ * Clean up old audit entries
198
+ */
199
+ cleanup(maxAgeMs = 7 * 24 * 60 * 60 * 1e3) {
200
+ try {
201
+ const cutoff = Date.now() - maxAgeMs;
202
+ const stmt = this.db.prepare(`
203
+ DELETE FROM retrieval_audit
204
+ WHERE project_id = ? AND timestamp < ?
205
+ `);
206
+ const result = stmt.run(this.projectId, cutoff);
207
+ logger.info("Cleaned up old audit entries", { deleted: result.changes });
208
+ return result.changes;
209
+ } catch (error) {
210
+ logger.warn("Failed to cleanup audit entries", {
211
+ error: error instanceof Error ? error.message : String(error)
212
+ });
213
+ return 0;
214
+ }
215
+ }
216
+ rowToEntry(row) {
217
+ return {
218
+ id: row.id,
219
+ timestamp: row.timestamp,
220
+ projectId: row.project_id,
221
+ query: row.query,
222
+ reasoning: row.reasoning,
223
+ framesRetrieved: JSON.parse(row.frames_retrieved),
224
+ confidenceScore: row.confidence_score,
225
+ provider: row.provider,
226
+ tokensUsed: row.tokens_used,
227
+ tokenBudget: row.token_budget,
228
+ analysisTimeMs: row.analysis_time_ms,
229
+ queryComplexity: row.query_complexity
230
+ };
231
+ }
232
+ }
233
+ export {
234
+ RetrievalAuditStore
235
+ };
236
+ //# sourceMappingURL=retrieval-audit.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/core/retrieval/retrieval-audit.ts"],
4
+ "sourcesContent": ["/**\n * Retrieval Audit Store\n * Records retrieval decisions for auditing and debugging\n */\n\nimport Database from 'better-sqlite3';\nimport { v4 as uuidv4 } from 'uuid';\nimport { logger } from '../monitoring/logger.js';\nimport { LLMAnalysisResponse } from './types.js';\n\n/**\n * A single retrieval audit entry\n */\nexport interface RetrievalAuditEntry {\n id: string;\n timestamp: number;\n projectId: string;\n query: string;\n reasoning: string;\n framesRetrieved: string[];\n confidenceScore: number;\n provider: 'anthropic' | 'heuristic' | 'cached';\n tokensUsed: number;\n tokenBudget: number;\n analysisTimeMs: number;\n queryComplexity: 'simple' | 'moderate' | 'complex';\n}\n\n/**\n * Stores retrieval audit entries for later inspection\n */\nexport class RetrievalAuditStore {\n private db: Database.Database;\n private projectId: string;\n private initialized = false;\n\n constructor(db: Database.Database, projectId: string) {\n this.db = db;\n this.projectId = projectId;\n this.initSchema();\n }\n\n /**\n * Initialize the audit table schema\n */\n private initSchema(): void {\n if (this.initialized) return;\n\n try {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS retrieval_audit (\n id TEXT PRIMARY KEY,\n timestamp INTEGER NOT NULL,\n project_id TEXT NOT NULL,\n query TEXT NOT NULL,\n reasoning TEXT NOT NULL,\n frames_retrieved TEXT NOT NULL,\n confidence_score REAL NOT NULL,\n provider TEXT NOT NULL,\n tokens_used INTEGER NOT NULL,\n token_budget INTEGER NOT NULL,\n analysis_time_ms INTEGER NOT NULL,\n query_complexity TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_retrieval_audit_project_time\n ON retrieval_audit(project_id, timestamp DESC);\n\n CREATE INDEX IF NOT EXISTS idx_retrieval_audit_query\n ON retrieval_audit(project_id, query);\n `);\n\n this.initialized = true;\n logger.debug('Retrieval audit schema initialized');\n } catch (error) {\n logger.warn('Failed to initialize retrieval audit schema', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n /**\n * Record a retrieval decision\n */\n record(\n query: string,\n analysis: LLMAnalysisResponse,\n options: {\n tokensUsed: number;\n tokenBudget: number;\n provider: 'anthropic' | 'heuristic' | 'cached';\n }\n ): string {\n const id = uuidv4();\n const timestamp = Date.now();\n\n try {\n const stmt = this.db.prepare(`\n INSERT INTO retrieval_audit (\n id, timestamp, project_id, query, reasoning, frames_retrieved,\n confidence_score, provider, tokens_used, token_budget,\n analysis_time_ms, query_complexity\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `);\n\n stmt.run(\n id,\n timestamp,\n this.projectId,\n query,\n analysis.reasoning,\n JSON.stringify(analysis.framesToRetrieve.map((f) => f.frameId)),\n analysis.confidenceScore,\n options.provider,\n options.tokensUsed,\n options.tokenBudget,\n analysis.metadata.analysisTimeMs,\n analysis.metadata.queryComplexity\n );\n\n logger.debug('Recorded retrieval audit entry', {\n id,\n query: query.slice(0, 50),\n });\n return id;\n } catch (error) {\n logger.warn('Failed to record retrieval audit', {\n error: error instanceof Error ? error.message : String(error),\n });\n return id; // Return ID even on failure\n }\n }\n\n /**\n * Get recent retrieval audit entries\n */\n getRecent(limit = 10): RetrievalAuditEntry[] {\n try {\n const stmt = this.db.prepare(`\n SELECT * FROM retrieval_audit\n WHERE project_id = ?\n ORDER BY timestamp DESC\n LIMIT ?\n `);\n\n const rows = stmt.all(this.projectId, limit) as any[];\n return rows.map(this.rowToEntry);\n } catch (error) {\n logger.warn('Failed to get recent audit entries', {\n error: error instanceof Error ? error.message : String(error),\n });\n return [];\n }\n }\n\n /**\n * Get audit entry by ID\n */\n getById(id: string): RetrievalAuditEntry | null {\n try {\n const stmt = this.db.prepare(`\n SELECT * FROM retrieval_audit WHERE id = ?\n `);\n\n const row = stmt.get(id) as any;\n return row ? this.rowToEntry(row) : null;\n } catch (error) {\n logger.warn('Failed to get audit entry', {\n error: error instanceof Error ? error.message : String(error),\n id,\n });\n return null;\n }\n }\n\n /**\n * Search audit entries by query text\n */\n searchByQuery(searchTerm: string, limit = 10): RetrievalAuditEntry[] {\n try {\n const stmt = this.db.prepare(`\n SELECT * FROM retrieval_audit\n WHERE project_id = ? AND query LIKE ?\n ORDER BY timestamp DESC\n LIMIT ?\n `);\n\n const rows = stmt.all(this.projectId, `%${searchTerm}%`, limit) as any[];\n return rows.map(this.rowToEntry);\n } catch (error) {\n logger.warn('Failed to search audit entries', {\n error: error instanceof Error ? error.message : String(error),\n });\n return [];\n }\n }\n\n /**\n * Get statistics about retrieval patterns\n */\n getStats(): {\n totalRetrievals: number;\n avgConfidence: number;\n providerBreakdown: Record<string, number>;\n avgTokensUsed: number;\n avgAnalysisTime: number;\n } {\n try {\n const statsStmt = this.db.prepare(`\n SELECT\n COUNT(*) as total,\n AVG(confidence_score) as avg_confidence,\n AVG(tokens_used) as avg_tokens,\n AVG(analysis_time_ms) as avg_time\n FROM retrieval_audit\n WHERE project_id = ?\n `);\n\n const providerStmt = this.db.prepare(`\n SELECT provider, COUNT(*) as count\n FROM retrieval_audit\n WHERE project_id = ?\n GROUP BY provider\n `);\n\n const stats = statsStmt.get(this.projectId) as any;\n const providers = providerStmt.all(this.projectId) as any[];\n\n const providerBreakdown: Record<string, number> = {};\n for (const p of providers) {\n providerBreakdown[p.provider] = p.count;\n }\n\n return {\n totalRetrievals: stats?.total || 0,\n avgConfidence: stats?.avg_confidence || 0,\n providerBreakdown,\n avgTokensUsed: stats?.avg_tokens || 0,\n avgAnalysisTime: stats?.avg_time || 0,\n };\n } catch (error) {\n logger.warn('Failed to get audit stats', {\n error: error instanceof Error ? error.message : String(error),\n });\n return {\n totalRetrievals: 0,\n avgConfidence: 0,\n providerBreakdown: {},\n avgTokensUsed: 0,\n avgAnalysisTime: 0,\n };\n }\n }\n\n /**\n * Clean up old audit entries\n */\n cleanup(maxAgeMs = 7 * 24 * 60 * 60 * 1000): number {\n try {\n const cutoff = Date.now() - maxAgeMs;\n const stmt = this.db.prepare(`\n DELETE FROM retrieval_audit\n WHERE project_id = ? AND timestamp < ?\n `);\n\n const result = stmt.run(this.projectId, cutoff);\n logger.info('Cleaned up old audit entries', { deleted: result.changes });\n return result.changes;\n } catch (error) {\n logger.warn('Failed to cleanup audit entries', {\n error: error instanceof Error ? error.message : String(error),\n });\n return 0;\n }\n }\n\n private rowToEntry(row: any): RetrievalAuditEntry {\n return {\n id: row.id,\n timestamp: row.timestamp,\n projectId: row.project_id,\n query: row.query,\n reasoning: row.reasoning,\n framesRetrieved: JSON.parse(row.frames_retrieved),\n confidenceScore: row.confidence_score,\n provider: row.provider as 'anthropic' | 'heuristic' | 'cached',\n tokensUsed: row.tokens_used,\n tokenBudget: row.token_budget,\n analysisTimeMs: row.analysis_time_ms,\n queryComplexity: row.query_complexity as\n | 'simple'\n | 'moderate'\n | 'complex',\n };\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,180 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { LinearClient } from "../integrations/linear/client.js";
6
+ import { LinearAuthManager } from "../integrations/linear/auth.js";
7
+ const TEST_KEYWORDS = [
8
+ "test",
9
+ "spec",
10
+ "unit test",
11
+ "integration test",
12
+ "e2e",
13
+ "end-to-end",
14
+ "jest",
15
+ "vitest",
16
+ "mocha"
17
+ ];
18
+ const VALIDATION_KEYWORDS = [
19
+ "validate",
20
+ "verify",
21
+ "verification",
22
+ "acceptance criteria",
23
+ "ac:",
24
+ "acceptance:",
25
+ "given when then",
26
+ "criteria:"
27
+ ];
28
+ const QA_KEYWORDS = ["qa", "quality", "regression", "coverage", "assertion"];
29
+ const TEST_LABELS = [
30
+ "needs-tests",
31
+ "test-required",
32
+ "qa-review",
33
+ "has-ac",
34
+ "acceptance-criteria",
35
+ "tdd",
36
+ "testing"
37
+ ];
38
+ function containsKeywords(text, keywords) {
39
+ const lowerText = text.toLowerCase();
40
+ return keywords.some((kw) => lowerText.includes(kw.toLowerCase()));
41
+ }
42
+ function scoreTask(issue, preferTestTasks) {
43
+ let score = 0;
44
+ const description = issue.description || "";
45
+ const title = issue.title || "";
46
+ const fullText = `${title} ${description}`;
47
+ if (containsKeywords(fullText, TEST_KEYWORDS)) {
48
+ score += preferTestTasks ? 10 : 5;
49
+ }
50
+ if (containsKeywords(fullText, VALIDATION_KEYWORDS)) {
51
+ score += preferTestTasks ? 8 : 4;
52
+ }
53
+ if (containsKeywords(fullText, QA_KEYWORDS)) {
54
+ score += preferTestTasks ? 5 : 2;
55
+ }
56
+ const labelNames = issue.labels?.nodes?.map((l) => l.name.toLowerCase()) || [];
57
+ const hasTestLabel = TEST_LABELS.some(
58
+ (tl) => labelNames.some((ln) => ln.includes(tl))
59
+ );
60
+ if (hasTestLabel) {
61
+ score += preferTestTasks ? 5 : 3;
62
+ }
63
+ if (issue.priority === 1) {
64
+ score += 5;
65
+ } else if (issue.priority === 2) {
66
+ score += 3;
67
+ } else if (issue.priority === 3) {
68
+ score += 1;
69
+ }
70
+ if (description.includes("## Acceptance") || description.includes("### AC") || description.includes("- [ ]")) {
71
+ score += 2;
72
+ }
73
+ if (issue.estimate) {
74
+ score += 1;
75
+ }
76
+ return score;
77
+ }
78
+ function getLinearClient() {
79
+ const apiKey = process.env["LINEAR_API_KEY"];
80
+ if (apiKey) {
81
+ return new LinearClient({ apiKey });
82
+ }
83
+ try {
84
+ const authManager = new LinearAuthManager();
85
+ const tokens = authManager.loadTokens();
86
+ if (tokens?.accessToken) {
87
+ return new LinearClient({ accessToken: tokens.accessToken });
88
+ }
89
+ } catch {
90
+ }
91
+ return null;
92
+ }
93
+ async function pickNextLinearTask(options = {}) {
94
+ const client = getLinearClient();
95
+ if (!client) {
96
+ return null;
97
+ }
98
+ const { teamId, preferTestTasks = true, limit = 20 } = options;
99
+ try {
100
+ const [backlogIssues, unstartedIssues] = await Promise.all([
101
+ client.getIssues({ teamId, stateType: "backlog", limit }),
102
+ client.getIssues({ teamId, stateType: "unstarted", limit })
103
+ ]);
104
+ const allIssues = [...backlogIssues, ...unstartedIssues];
105
+ const unassignedIssues = allIssues.filter((issue) => !issue.assignee);
106
+ if (unassignedIssues.length === 0) {
107
+ if (allIssues.length === 0) {
108
+ return null;
109
+ }
110
+ }
111
+ const issuesToScore = unassignedIssues.length > 0 ? unassignedIssues : allIssues;
112
+ const scoredIssues = issuesToScore.map((issue) => ({
113
+ issue,
114
+ score: scoreTask(issue, preferTestTasks)
115
+ }));
116
+ scoredIssues.sort((a, b) => b.score - a.score);
117
+ const best = scoredIssues[0];
118
+ if (!best) {
119
+ return null;
120
+ }
121
+ const description = best.issue.description || "";
122
+ const hasTestRequirements = containsKeywords(description, TEST_KEYWORDS) || containsKeywords(description, VALIDATION_KEYWORDS);
123
+ return {
124
+ id: best.issue.id,
125
+ identifier: best.issue.identifier,
126
+ title: best.issue.title,
127
+ priority: best.issue.priority,
128
+ hasTestRequirements,
129
+ estimatedPoints: best.issue.estimate,
130
+ url: best.issue.url,
131
+ score: best.score
132
+ };
133
+ } catch (error) {
134
+ console.error("[linear-task-picker] Error fetching tasks:", error);
135
+ return null;
136
+ }
137
+ }
138
+ async function getTopTaskSuggestions(options = {}, count = 3) {
139
+ const client = getLinearClient();
140
+ if (!client) {
141
+ return [];
142
+ }
143
+ const { teamId, preferTestTasks = true, limit = 30 } = options;
144
+ try {
145
+ const [backlogIssues, unstartedIssues] = await Promise.all([
146
+ client.getIssues({ teamId, stateType: "backlog", limit }),
147
+ client.getIssues({ teamId, stateType: "unstarted", limit })
148
+ ]);
149
+ const allIssues = [...backlogIssues, ...unstartedIssues];
150
+ const unassignedIssues = allIssues.filter((issue) => !issue.assignee);
151
+ const issuesToScore = unassignedIssues.length > 0 ? unassignedIssues : allIssues;
152
+ const scoredIssues = issuesToScore.map((issue) => ({
153
+ issue,
154
+ score: scoreTask(issue, preferTestTasks)
155
+ }));
156
+ scoredIssues.sort((a, b) => b.score - a.score);
157
+ return scoredIssues.slice(0, count).map(({ issue, score }) => {
158
+ const description = issue.description || "";
159
+ const hasTestRequirements = containsKeywords(description, TEST_KEYWORDS) || containsKeywords(description, VALIDATION_KEYWORDS);
160
+ return {
161
+ id: issue.id,
162
+ identifier: issue.identifier,
163
+ title: issue.title,
164
+ priority: issue.priority,
165
+ hasTestRequirements,
166
+ estimatedPoints: issue.estimate,
167
+ url: issue.url,
168
+ score
169
+ };
170
+ });
171
+ } catch (error) {
172
+ console.error("[linear-task-picker] Error fetching tasks:", error);
173
+ return [];
174
+ }
175
+ }
176
+ export {
177
+ getTopTaskSuggestions,
178
+ pickNextLinearTask
179
+ };
180
+ //# sourceMappingURL=linear-task-picker.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/hooks/linear-task-picker.ts"],
4
+ "sourcesContent": ["/**\n * Linear Task Picker\n * Picks the next best task from Linear queue, prioritizing tasks with test/validation requirements\n */\n\nimport { LinearClient, LinearIssue } from '../integrations/linear/client.js';\nimport { LinearAuthManager } from '../integrations/linear/auth.js';\n\nexport interface TaskSuggestion {\n id: string;\n identifier: string; // e.g., \"STA-123\"\n title: string;\n priority: number;\n hasTestRequirements: boolean;\n estimatedPoints?: number;\n url: string;\n score: number;\n}\n\nexport interface PickerOptions {\n teamId?: string;\n preferTestTasks?: boolean;\n limit?: number;\n}\n\n// Keywords indicating test/validation requirements\nconst TEST_KEYWORDS = [\n 'test',\n 'spec',\n 'unit test',\n 'integration test',\n 'e2e',\n 'end-to-end',\n 'jest',\n 'vitest',\n 'mocha',\n];\n\nconst VALIDATION_KEYWORDS = [\n 'validate',\n 'verify',\n 'verification',\n 'acceptance criteria',\n 'ac:',\n 'acceptance:',\n 'given when then',\n 'criteria:',\n];\n\nconst QA_KEYWORDS = ['qa', 'quality', 'regression', 'coverage', 'assertion'];\n\n// Labels that indicate test requirements\nconst TEST_LABELS = [\n 'needs-tests',\n 'test-required',\n 'qa-review',\n 'has-ac',\n 'acceptance-criteria',\n 'tdd',\n 'testing',\n];\n\n/**\n * Check if text contains any of the keywords (case-insensitive)\n */\nfunction containsKeywords(text: string, keywords: string[]): boolean {\n const lowerText = text.toLowerCase();\n return keywords.some((kw) => lowerText.includes(kw.toLowerCase()));\n}\n\n/**\n * Score a task based on test/validation requirements\n */\nfunction scoreTask(issue: LinearIssue, preferTestTasks: boolean): number {\n let score = 0;\n const description = issue.description || '';\n const title = issue.title || '';\n const fullText = `${title} ${description}`;\n\n // +10 if has test/validation keywords in description\n if (containsKeywords(fullText, TEST_KEYWORDS)) {\n score += preferTestTasks ? 10 : 5;\n }\n\n if (containsKeywords(fullText, VALIDATION_KEYWORDS)) {\n score += preferTestTasks ? 8 : 4;\n }\n\n if (containsKeywords(fullText, QA_KEYWORDS)) {\n score += preferTestTasks ? 5 : 2;\n }\n\n // +5 if has test-related labels\n const labelNames =\n issue.labels?.nodes?.map((l: { name: string }) => l.name.toLowerCase()) ||\n [];\n const hasTestLabel = TEST_LABELS.some((tl) =>\n labelNames.some((ln: string) => ln.includes(tl))\n );\n if (hasTestLabel) {\n score += preferTestTasks ? 5 : 3;\n }\n\n // +3 for higher priority (urgent=1, high=2)\n if (issue.priority === 1) {\n score += 5; // Urgent\n } else if (issue.priority === 2) {\n score += 3; // High\n } else if (issue.priority === 3) {\n score += 1; // Medium\n }\n\n // +2 if has acceptance criteria pattern\n if (\n description.includes('## Acceptance') ||\n description.includes('### AC') ||\n description.includes('- [ ]')\n ) {\n score += 2;\n }\n\n // +1 if has estimate (indicates well-scoped)\n if (issue.estimate) {\n score += 1;\n }\n\n return score;\n}\n\n/**\n * Get Linear client instance\n */\nfunction getLinearClient(): LinearClient | null {\n // Try API key first\n const apiKey = process.env['LINEAR_API_KEY'];\n if (apiKey) {\n return new LinearClient({ apiKey });\n }\n\n // Fall back to OAuth\n try {\n const authManager = new LinearAuthManager();\n const tokens = authManager.loadTokens();\n if (tokens?.accessToken) {\n return new LinearClient({ accessToken: tokens.accessToken });\n }\n } catch {\n // Auth not available\n }\n\n return null;\n}\n\n/**\n * Pick the next best task from Linear\n */\nexport async function pickNextLinearTask(\n options: PickerOptions = {}\n): Promise<TaskSuggestion | null> {\n const client = getLinearClient();\n if (!client) {\n return null;\n }\n\n const { teamId, preferTestTasks = true, limit = 20 } = options;\n\n try {\n // Fetch backlog and unstarted issues\n const [backlogIssues, unstartedIssues] = await Promise.all([\n client.getIssues({ teamId, stateType: 'backlog', limit }),\n client.getIssues({ teamId, stateType: 'unstarted', limit }),\n ]);\n\n const allIssues = [...backlogIssues, ...unstartedIssues];\n\n // Filter out assigned issues (we want unassigned ones)\n const unassignedIssues = allIssues.filter((issue) => !issue.assignee);\n\n if (unassignedIssues.length === 0) {\n // If no unassigned, consider all\n if (allIssues.length === 0) {\n return null;\n }\n }\n\n const issuesToScore =\n unassignedIssues.length > 0 ? unassignedIssues : allIssues;\n\n // Score and sort\n const scoredIssues = issuesToScore.map((issue) => ({\n issue,\n score: scoreTask(issue, preferTestTasks),\n }));\n\n scoredIssues.sort((a, b) => b.score - a.score);\n\n const best = scoredIssues[0];\n if (!best) {\n return null;\n }\n\n const description = best.issue.description || '';\n const hasTestRequirements =\n containsKeywords(description, TEST_KEYWORDS) ||\n containsKeywords(description, VALIDATION_KEYWORDS);\n\n return {\n id: best.issue.id,\n identifier: best.issue.identifier,\n title: best.issue.title,\n priority: best.issue.priority,\n hasTestRequirements,\n estimatedPoints: best.issue.estimate,\n url: best.issue.url,\n score: best.score,\n };\n } catch (error) {\n console.error('[linear-task-picker] Error fetching tasks:', error);\n return null;\n }\n}\n\n/**\n * Get multiple task suggestions (for showing options)\n */\nexport async function getTopTaskSuggestions(\n options: PickerOptions = {},\n count: number = 3\n): Promise<TaskSuggestion[]> {\n const client = getLinearClient();\n if (!client) {\n return [];\n }\n\n const { teamId, preferTestTasks = true, limit = 30 } = options;\n\n try {\n const [backlogIssues, unstartedIssues] = await Promise.all([\n client.getIssues({ teamId, stateType: 'backlog', limit }),\n client.getIssues({ teamId, stateType: 'unstarted', limit }),\n ]);\n\n const allIssues = [...backlogIssues, ...unstartedIssues];\n const unassignedIssues = allIssues.filter((issue) => !issue.assignee);\n const issuesToScore =\n unassignedIssues.length > 0 ? unassignedIssues : allIssues;\n\n const scoredIssues = issuesToScore.map((issue) => ({\n issue,\n score: scoreTask(issue, preferTestTasks),\n }));\n\n scoredIssues.sort((a, b) => b.score - a.score);\n\n return scoredIssues.slice(0, count).map(({ issue, score }) => {\n const description = issue.description || '';\n const hasTestRequirements =\n containsKeywords(description, TEST_KEYWORDS) ||\n containsKeywords(description, VALIDATION_KEYWORDS);\n\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n priority: issue.priority,\n hasTestRequirements,\n estimatedPoints: issue.estimate,\n url: issue.url,\n score,\n };\n });\n } catch (error) {\n console.error('[linear-task-picker] Error fetching tasks:', error);\n return [];\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }