@stackmemoryai/stackmemory 0.5.40 → 0.5.42

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/README.md CHANGED
@@ -1,19 +1,15 @@
1
1
  # StackMemory
2
2
 
3
- **Lossless, project-scoped memory for AI tools** • v0.5.30
3
+ **Lossless, project-scoped memory for AI tools** • v0.5.40
4
4
 
5
- StackMemory is a **production-ready memory runtime** for AI coding tools that preserves full project context across sessions. With **Phases 1-4 complete**, it delivers:
5
+ StackMemory is a **production-ready memory runtime** for AI coding tools that preserves full project context across sessions:
6
6
 
7
- - **89-98% faster** task operations than manual tracking
8
- - **10,000+ frame depth** support with hierarchical organization
7
+ - **Zero-config setup** - `stackmemory init` just works, no questions asked
8
+ - **26 MCP tools** for Claude Code integration
9
9
  - **Full Linear integration** with bidirectional sync
10
- - **20+ MCP tools** for Claude Code
11
10
  - **Context persistence** that survives /clear operations
12
- - **Two-tier storage system** with local tiers and infinite remote storage
13
- - **Smart compression** (LZ4/ZSTD) with 2.5-3.5x ratios
14
- - **Background migration** with configurable triggers
15
- - **396 tests passing** with standardized error handling
16
- - **npm v0.5.30** published with WhatsApp notifications and improved integrations
11
+ - **Hierarchical frame organization** (nested call stack model)
12
+ - **490 tests passing** with comprehensive coverage
17
13
 
18
14
  Instead of a linear chat log, StackMemory organizes memory as a **call stack** of scoped work (frames), with intelligent LLM-driven retrieval and team collaboration features.
19
15
 
@@ -120,46 +116,76 @@ The editor never manages memory directly; it asks StackMemory for the **context
120
116
 
121
117
  # QuickStart
122
118
 
123
- ## 1. Hosted (Recommended)
124
-
125
- ### Step 1: Create a project
119
+ ## 1. Install & Initialize (30 seconds)
126
120
 
127
121
  ```bash
128
- stackmemory projects create \
129
- --repo https://github.com/org/repo
122
+ # Install globally
123
+ npm install -g @stackmemoryai/stackmemory
124
+
125
+ # Initialize in your project (zero-config, just works)
126
+ cd your-project
127
+ stackmemory init
128
+
129
+ # Configure Claude Code integration
130
+ stackmemory setup-mcp
131
+
132
+ # Verify everything works
133
+ stackmemory doctor
130
134
  ```
131
135
 
132
- This creates a **project-scoped memory space** tied to the repo.
136
+ That's it! Restart Claude Code and StackMemory MCP tools are available.
133
137
 
134
138
  ---
135
139
 
136
- ### Step 2: Install StackMemory
140
+ ## 2. Detailed Setup
141
+
142
+ ### Install
137
143
 
138
144
  ```bash
139
- npm install -g @stackmemoryai/stackmemory@0.5.30
140
- # or latest
141
145
  npm install -g @stackmemoryai/stackmemory@latest
142
146
  ```
143
147
 
144
- ---
148
+ During install, you'll be asked if you want to install Claude Code hooks (optional but recommended).
145
149
 
146
- ### Step 3: Setup Claude Code Integration (Automated)
150
+ ### Initialize Project
147
151
 
148
152
  ```bash
149
- # Automatic setup - configures MCP and session hooks
150
- npm run claude:setup
153
+ cd your-project
154
+ stackmemory init
151
155
  ```
152
156
 
153
- This automatically:
157
+ This creates `.stackmemory/` with SQLite storage. No questions asked.
158
+
159
+ For interactive setup with more options:
160
+ ```bash
161
+ stackmemory init --interactive
162
+ ```
154
163
 
164
+ ### Configure Claude Code
165
+
166
+ ```bash
167
+ stackmemory setup-mcp
168
+ ```
169
+
170
+ This automatically:
155
171
  - Creates `~/.claude/stackmemory-mcp.json` MCP configuration
156
- - Sets up session initialization hooks
157
172
  - Updates `~/.claude/config.json` with StackMemory integration
173
+ - Validates the configuration
174
+
175
+ ### Diagnose Issues
176
+
177
+ ```bash
178
+ stackmemory doctor
179
+ ```
158
180
 
159
- **Manual setup alternative:**
181
+ Checks project initialization, database integrity, MCP config, and suggests fixes.
182
+
183
+ ---
184
+
185
+ ## 3. Advanced Setup
160
186
 
161
187
  <details>
162
- <summary>Click to expand manual setup steps</summary>
188
+ <summary>Manual MCP configuration</summary>
163
189
 
164
190
  Create MCP configuration:
165
191
 
@@ -178,7 +204,7 @@ cat > ~/.claude/stackmemory-mcp.json << 'EOF'
178
204
  EOF
179
205
  ```
180
206
 
181
- Update Claude config:
207
+ Update Claude config.json:
182
208
 
183
209
  ```json
184
210
  {
@@ -190,6 +216,26 @@ Update Claude config:
190
216
 
191
217
  </details>
192
218
 
219
+ <details>
220
+ <summary>Hosted mode (cloud storage)</summary>
221
+
222
+ ```bash
223
+ stackmemory onboard
224
+ # Select "Hosted" when prompted
225
+ # Or set DATABASE_URL environment variable
226
+ ```
227
+
228
+ </details>
229
+
230
+ <details>
231
+ <summary>ChromaDB for semantic search</summary>
232
+
233
+ ```bash
234
+ stackmemory init --chromadb
235
+ # Prompts for ChromaDB API key
236
+ ```
237
+
238
+ </details>
193
239
 
194
240
  Claude Code sessions automatically capture tool calls, maintain context across sessions, and sync with Linear when configured.
195
241
 
@@ -409,8 +455,13 @@ stackmemory skills rlm "Build, test, and publish version 2.0.0"
409
455
  ## CLI Commands
410
456
 
411
457
  ```bash
412
- # Core
413
- stackmemory init # Initialize project
458
+ # Setup & Diagnostics
459
+ stackmemory init # Initialize project (zero-config)
460
+ stackmemory init --interactive # Interactive setup with options
461
+ stackmemory setup-mcp # Configure Claude Code MCP
462
+ stackmemory doctor # Diagnose issues and suggest fixes
463
+
464
+ # Status
414
465
  stackmemory status # Current status
415
466
  stackmemory progress # Recent activity
416
467
 
@@ -455,20 +506,33 @@ stackmemory mcp-server [--port 3001]
455
506
  - Hosted: **Private beta**
456
507
  - OSS mirror: **Production ready**
457
508
  - MCP integration: **Stable**
458
- - CLI: **v0.5.30** - Full task, context, Linear, and storage management
509
+ - CLI: **v0.5.40** - Zero-config setup, diagnostics, full task/context/Linear management
459
510
  - Two-tier storage: **Complete**
460
- - Test Suite: **396 tests passing**
511
+ - Test Suite: **490 tests passing**
461
512
 
462
513
  ---
463
514
 
464
515
  ## Changelog
465
516
 
517
+ ### v0.5.40 (2026-01-27)
518
+ - **Zero-config onboarding**: `stackmemory init` now works without any prompts
519
+ - **New `setup-mcp` command**: Auto-configures Claude Code MCP integration
520
+ - **New `doctor` command**: Diagnoses issues and suggests fixes
521
+ - **Interactive postinstall**: Asks for consent before modifying ~/.claude
522
+ - **Better error messages**: Shows reason + fix + next step
523
+ - 490 tests passing
524
+
525
+ ### v0.5.39 (2026-01-27)
526
+ - **AsyncMutex**: Thread-safe Linear sync with stale lock detection
527
+ - **Action timeout**: 60s timeout for SMS action execution
528
+ - **Persistent rate limiting**: Survives server restarts
529
+ - **Atomic file writes**: Prevents corruption on crash
530
+
466
531
  ### v0.5.30 (2026-01-26)
467
532
  - Standardized error handling with `IntegrationError`, `DatabaseError`, `ValidationError`
468
533
  - Adopted error classes across Linear integration (12 files)
469
534
  - Adopted error classes across database layer (6 files)
470
535
  - WhatsApp notifications with session ID and interactive options
471
- - 396 tests passing with improved code quality
472
536
 
473
537
  ### v0.5.28 (2026-01-25)
474
538
  - WhatsApp flag for claude-sm automatic notifications
@@ -131,7 +131,10 @@ async function pickNextLinearTask(options = {}) {
131
131
  score: best.score
132
132
  };
133
133
  } catch (error) {
134
- console.error("[linear-task-picker] Error fetching tasks:", error);
134
+ const isAuthError = error instanceof Error && (error.message.includes("401") || error.message.includes("403"));
135
+ if (!isAuthError) {
136
+ console.error("[linear-task-picker] Error fetching tasks:", error);
137
+ }
135
138
  return null;
136
139
  }
137
140
  }
@@ -169,7 +172,10 @@ async function getTopTaskSuggestions(options = {}, count = 3) {
169
172
  };
170
173
  });
171
174
  } catch (error) {
172
- console.error("[linear-task-picker] Error fetching tasks:", error);
175
+ const isAuthError = error instanceof Error && (error.message.includes("401") || error.message.includes("403"));
176
+ if (!isAuthError) {
177
+ console.error("[linear-task-picker] Error fetching tasks:", error);
178
+ }
173
179
  return [];
174
180
  }
175
181
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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 * Returns null if credentials are missing or invalid\n */\nfunction getLinearClient(): LinearClient | null {\n // Try API key first - must be valid format (lin_api_*)\n const apiKey = process.env['LINEAR_API_KEY'];\n if (apiKey && apiKey.startsWith('lin_api_')) {\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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",
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 * Returns null if credentials are missing or invalid\n */\nfunction getLinearClient(): LinearClient | null {\n // Try API key first - must be valid format (lin_api_*)\n const apiKey = process.env['LINEAR_API_KEY'];\n if (apiKey && apiKey.startsWith('lin_api_')) {\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 // Silent fail for auth errors (401/403) - expected when not configured\n const isAuthError =\n error instanceof Error &&\n (error.message.includes('401') || error.message.includes('403'));\n if (!isAuthError) {\n console.error('[linear-task-picker] Error fetching tasks:', error);\n }\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 // Silent fail for auth errors (401/403) - expected when not configured\n const isAuthError =\n error instanceof Error &&\n (error.message.includes('401') || error.message.includes('403'));\n if (!isAuthError) {\n console.error('[linear-task-picker] Error fetching tasks:', error);\n }\n return [];\n }\n}\n"],
5
+ "mappings": ";;;;AAKA,SAAS,oBAAiC;AAC1C,SAAS,yBAAyB;AAoBlC,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,sBAAsB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,cAAc,CAAC,MAAM,WAAW,cAAc,YAAY,WAAW;AAG3E,MAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKA,SAAS,iBAAiB,MAAc,UAA6B;AACnE,QAAM,YAAY,KAAK,YAAY;AACnC,SAAO,SAAS,KAAK,CAAC,OAAO,UAAU,SAAS,GAAG,YAAY,CAAC,CAAC;AACnE;AAKA,SAAS,UAAU,OAAoB,iBAAkC;AACvE,MAAI,QAAQ;AACZ,QAAM,cAAc,MAAM,eAAe;AACzC,QAAM,QAAQ,MAAM,SAAS;AAC7B,QAAM,WAAW,GAAG,KAAK,IAAI,WAAW;AAGxC,MAAI,iBAAiB,UAAU,aAAa,GAAG;AAC7C,aAAS,kBAAkB,KAAK;AAAA,EAClC;AAEA,MAAI,iBAAiB,UAAU,mBAAmB,GAAG;AACnD,aAAS,kBAAkB,IAAI;AAAA,EACjC;AAEA,MAAI,iBAAiB,UAAU,WAAW,GAAG;AAC3C,aAAS,kBAAkB,IAAI;AAAA,EACjC;AAGA,QAAM,aACJ,MAAM,QAAQ,OAAO,IAAI,CAAC,MAAwB,EAAE,KAAK,YAAY,CAAC,KACtE,CAAC;AACH,QAAM,eAAe,YAAY;AAAA,IAAK,CAAC,OACrC,WAAW,KAAK,CAAC,OAAe,GAAG,SAAS,EAAE,CAAC;AAAA,EACjD;AACA,MAAI,cAAc;AAChB,aAAS,kBAAkB,IAAI;AAAA,EACjC;AAGA,MAAI,MAAM,aAAa,GAAG;AACxB,aAAS;AAAA,EACX,WAAW,MAAM,aAAa,GAAG;AAC/B,aAAS;AAAA,EACX,WAAW,MAAM,aAAa,GAAG;AAC/B,aAAS;AAAA,EACX;AAGA,MACE,YAAY,SAAS,eAAe,KACpC,YAAY,SAAS,QAAQ,KAC7B,YAAY,SAAS,OAAO,GAC5B;AACA,aAAS;AAAA,EACX;AAGA,MAAI,MAAM,UAAU;AAClB,aAAS;AAAA,EACX;AAEA,SAAO;AACT;AAMA,SAAS,kBAAuC;AAE9C,QAAM,SAAS,QAAQ,IAAI,gBAAgB;AAC3C,MAAI,UAAU,OAAO,WAAW,UAAU,GAAG;AAC3C,WAAO,IAAI,aAAa,EAAE,OAAO,CAAC;AAAA,EACpC;AAGA,MAAI;AACF,UAAM,cAAc,IAAI,kBAAkB;AAC1C,UAAM,SAAS,YAAY,WAAW;AACtC,QAAI,QAAQ,aAAa;AACvB,aAAO,IAAI,aAAa,EAAE,aAAa,OAAO,YAAY,CAAC;AAAA,IAC7D;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;AAKA,eAAsB,mBACpB,UAAyB,CAAC,GACM;AAChC,QAAM,SAAS,gBAAgB;AAC/B,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,EACT;AAEA,QAAM,EAAE,QAAQ,kBAAkB,MAAM,QAAQ,GAAG,IAAI;AAEvD,MAAI;AAEF,UAAM,CAAC,eAAe,eAAe,IAAI,MAAM,QAAQ,IAAI;AAAA,MACzD,OAAO,UAAU,EAAE,QAAQ,WAAW,WAAW,MAAM,CAAC;AAAA,MACxD,OAAO,UAAU,EAAE,QAAQ,WAAW,aAAa,MAAM,CAAC;AAAA,IAC5D,CAAC;AAED,UAAM,YAAY,CAAC,GAAG,eAAe,GAAG,eAAe;AAGvD,UAAM,mBAAmB,UAAU,OAAO,CAAC,UAAU,CAAC,MAAM,QAAQ;AAEpE,QAAI,iBAAiB,WAAW,GAAG;AAEjC,UAAI,UAAU,WAAW,GAAG;AAC1B,eAAO;AAAA,MACT;AAAA,IACF;AAEA,UAAM,gBACJ,iBAAiB,SAAS,IAAI,mBAAmB;AAGnD,UAAM,eAAe,cAAc,IAAI,CAAC,WAAW;AAAA,MACjD;AAAA,MACA,OAAO,UAAU,OAAO,eAAe;AAAA,IACzC,EAAE;AAEF,iBAAa,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAE7C,UAAM,OAAO,aAAa,CAAC;AAC3B,QAAI,CAAC,MAAM;AACT,aAAO;AAAA,IACT;AAEA,UAAM,cAAc,KAAK,MAAM,eAAe;AAC9C,UAAM,sBACJ,iBAAiB,aAAa,aAAa,KAC3C,iBAAiB,aAAa,mBAAmB;AAEnD,WAAO;AAAA,MACL,IAAI,KAAK,MAAM;AAAA,MACf,YAAY,KAAK,MAAM;AAAA,MACvB,OAAO,KAAK,MAAM;AAAA,MAClB,UAAU,KAAK,MAAM;AAAA,MACrB;AAAA,MACA,iBAAiB,KAAK,MAAM;AAAA,MAC5B,KAAK,KAAK,MAAM;AAAA,MAChB,OAAO,KAAK;AAAA,IACd;AAAA,EACF,SAAS,OAAO;AAEd,UAAM,cACJ,iBAAiB,UAChB,MAAM,QAAQ,SAAS,KAAK,KAAK,MAAM,QAAQ,SAAS,KAAK;AAChE,QAAI,CAAC,aAAa;AAChB,cAAQ,MAAM,8CAA8C,KAAK;AAAA,IACnE;AACA,WAAO;AAAA,EACT;AACF;AAKA,eAAsB,sBACpB,UAAyB,CAAC,GAC1B,QAAgB,GACW;AAC3B,QAAM,SAAS,gBAAgB;AAC/B,MAAI,CAAC,QAAQ;AACX,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,EAAE,QAAQ,kBAAkB,MAAM,QAAQ,GAAG,IAAI;AAEvD,MAAI;AACF,UAAM,CAAC,eAAe,eAAe,IAAI,MAAM,QAAQ,IAAI;AAAA,MACzD,OAAO,UAAU,EAAE,QAAQ,WAAW,WAAW,MAAM,CAAC;AAAA,MACxD,OAAO,UAAU,EAAE,QAAQ,WAAW,aAAa,MAAM,CAAC;AAAA,IAC5D,CAAC;AAED,UAAM,YAAY,CAAC,GAAG,eAAe,GAAG,eAAe;AACvD,UAAM,mBAAmB,UAAU,OAAO,CAAC,UAAU,CAAC,MAAM,QAAQ;AACpE,UAAM,gBACJ,iBAAiB,SAAS,IAAI,mBAAmB;AAEnD,UAAM,eAAe,cAAc,IAAI,CAAC,WAAW;AAAA,MACjD;AAAA,MACA,OAAO,UAAU,OAAO,eAAe;AAAA,IACzC,EAAE;AAEF,iBAAa,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAE7C,WAAO,aAAa,MAAM,GAAG,KAAK,EAAE,IAAI,CAAC,EAAE,OAAO,MAAM,MAAM;AAC5D,YAAM,cAAc,MAAM,eAAe;AACzC,YAAM,sBACJ,iBAAiB,aAAa,aAAa,KAC3C,iBAAiB,aAAa,mBAAmB;AAEnD,aAAO;AAAA,QACL,IAAI,MAAM;AAAA,QACV,YAAY,MAAM;AAAA,QAClB,OAAO,MAAM;AAAA,QACb,UAAU,MAAM;AAAA,QAChB;AAAA,QACA,iBAAiB,MAAM;AAAA,QACvB,KAAK,MAAM;AAAA,QACX;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AAEd,UAAM,cACJ,iBAAiB,UAChB,MAAM,QAAQ,SAAS,KAAK,KAAK,MAAM,QAAQ,SAAS,KAAK;AAChE,QAAI,CAAC,aAAa;AAChB,cAAQ,MAAM,8CAA8C,KAAK;AAAA,IACnE;AACA,WAAO,CAAC;AAAA,EACV;AACF;",
6
6
  "names": []
7
7
  }
@@ -127,10 +127,12 @@ class LinearClient {
127
127
  }
128
128
  if (!response.ok) {
129
129
  const errorText = await response.text();
130
- logger.error(
131
- "Linear API error response:",
132
- new Error(`${response.status}: ${errorText}`)
133
- );
130
+ if (response.status !== 401 && response.status !== 403) {
131
+ logger.error(
132
+ "Linear API error response:",
133
+ new Error(`${response.status}: ${errorText}`)
134
+ );
135
+ }
134
136
  throw new IntegrationError(
135
137
  `Linear API error: ${response.status} ${response.statusText}`,
136
138
  ErrorCode.LINEAR_API_ERROR,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/integrations/linear/client.ts"],
4
- "sourcesContent": ["/**\n * Linear API Client for StackMemory\n * Handles bi-directional sync with Linear's GraphQL API\n */\n\nimport { logger } from '../../core/monitoring/logger.js';\nimport { IntegrationError, ErrorCode } from '../../core/errors/index.js';\n\nexport interface LinearConfig {\n apiKey: string;\n teamId?: string;\n webhookSecret?: string;\n baseUrl?: string;\n // If true, send Authorization header as `Bearer <apiKey>` (OAuth access token)\n useBearer?: boolean;\n // Optional callback to refresh token on 401 and return the new access token\n onUnauthorized?: () => Promise<string>;\n}\n\nexport interface LinearIssue {\n id: string;\n identifier: string; // Like \"SM-123\"\n title: string;\n description?: string;\n state: {\n id: string;\n name: string;\n type: 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled';\n };\n priority: number; // 0-4 (0=none, 1=urgent, 2=high, 3=medium, 4=low)\n assignee?: {\n id: string;\n name: string;\n email: string;\n };\n estimate?: number; // Story points\n labels: Array<{\n id: string;\n name: string;\n }>;\n createdAt: string;\n updatedAt: string;\n url: string;\n}\n\nexport interface LinearCreateIssueInput {\n title: string;\n description?: string;\n teamId: string;\n priority?: number;\n estimate?: number;\n labelIds?: string[];\n}\n\ninterface RateLimitState {\n remaining: number;\n resetAt: number;\n retryAfter: number;\n}\n\nexport class LinearClient {\n private config: LinearConfig;\n private baseUrl: string;\n private rateLimitState: RateLimitState = {\n remaining: 1500, // Linear's default limit\n resetAt: Date.now() + 3600000,\n retryAfter: 0,\n };\n private requestQueue: Array<() => Promise<void>> = [];\n private isProcessingQueue = false;\n private minRequestInterval = 100; // Minimum ms between requests\n private lastRequestTime = 0;\n\n constructor(config: LinearConfig) {\n this.config = config;\n this.baseUrl = config.baseUrl || 'https://api.linear.app';\n\n if (!config.apiKey) {\n throw new IntegrationError(\n 'Linear API key is required',\n ErrorCode.LINEAR_AUTH_FAILED\n );\n }\n }\n\n /**\n * Wait for rate limit to reset if needed\n */\n private async waitForRateLimit(): Promise<void> {\n const now = Date.now();\n\n // Check if we're in a retry-after period\n if (this.rateLimitState.retryAfter > now) {\n const waitTime = this.rateLimitState.retryAfter - now;\n logger.warn(`Rate limited, waiting ${Math.ceil(waitTime / 1000)}s`);\n await this.sleep(waitTime);\n }\n\n // Check if we've exhausted our rate limit\n if (this.rateLimitState.remaining <= 5) {\n if (this.rateLimitState.resetAt > now) {\n const waitTime = this.rateLimitState.resetAt - now;\n logger.warn(\n `Rate limit nearly exhausted, waiting ${Math.ceil(waitTime / 1000)}s for reset`\n );\n await this.sleep(Math.min(waitTime, 60000)); // Max 60s wait\n }\n }\n\n // Ensure minimum interval between requests\n const timeSinceLastRequest = now - this.lastRequestTime;\n if (timeSinceLastRequest < this.minRequestInterval) {\n await this.sleep(this.minRequestInterval - timeSinceLastRequest);\n }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n /**\n * Update rate limit state from response headers\n */\n private updateRateLimitState(response: Response): void {\n const remaining = response.headers.get('x-ratelimit-remaining');\n const reset = response.headers.get('x-ratelimit-reset');\n const retryAfter = response.headers.get('retry-after');\n\n if (remaining !== null) {\n this.rateLimitState.remaining = parseInt(remaining, 10);\n }\n if (reset !== null) {\n this.rateLimitState.resetAt = parseInt(reset, 10) * 1000;\n }\n if (retryAfter !== null) {\n this.rateLimitState.retryAfter =\n Date.now() + parseInt(retryAfter, 10) * 1000;\n }\n }\n\n /**\n * Execute GraphQL query against Linear API with rate limiting\n */\n private async graphql<T>(\n query: string,\n variables?: Record<string, unknown>,\n retries = 3,\n allowAuthRefresh = true\n ): Promise<T> {\n // Wait for rate limit before making request\n await this.waitForRateLimit();\n\n this.lastRequestTime = Date.now();\n\n const authHeader = this.config.useBearer\n ? `Bearer ${this.config.apiKey}`\n : this.config.apiKey;\n\n let response = await fetch(`${this.baseUrl}/graphql`, {\n method: 'POST',\n headers: {\n Authorization: authHeader,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n query,\n variables,\n }),\n });\n\n // Update rate limit state from response\n this.updateRateLimitState(response);\n\n // Handle unauthorized (e.g., expired OAuth token)\n if (\n response.status === 401 &&\n this.config.onUnauthorized &&\n allowAuthRefresh\n ) {\n try {\n const newToken = await this.config.onUnauthorized();\n // Update local config and retry once without further auth refresh\n this.config.apiKey = newToken;\n const retryHeader = this.config.useBearer\n ? `Bearer ${newToken}`\n : newToken;\n response = await fetch(`${this.baseUrl}/graphql`, {\n method: 'POST',\n headers: {\n Authorization: retryHeader,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ query, variables }),\n });\n this.updateRateLimitState(response);\n } catch (e: unknown) {\n // Fall through to standard error handling\n }\n }\n\n // Handle rate limiting with exponential backoff\n if (response.status === 429) {\n if (retries > 0) {\n const retryAfter = response.headers.get('retry-after');\n const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : 60000;\n logger.warn(\n `Rate limited (429), retrying in ${waitTime / 1000}s (${retries} retries left)`\n );\n this.rateLimitState.retryAfter = Date.now() + waitTime;\n await this.sleep(waitTime);\n return this.graphql<T>(query, variables, retries - 1, allowAuthRefresh);\n }\n throw new IntegrationError(\n 'Linear API rate limit exceeded after retries',\n ErrorCode.LINEAR_API_ERROR,\n { retries: 0 }\n );\n }\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(\n 'Linear API error response:',\n new Error(`${response.status}: ${errorText}`)\n );\n throw new IntegrationError(\n `Linear API error: ${response.status} ${response.statusText}`,\n ErrorCode.LINEAR_API_ERROR,\n {\n status: response.status,\n statusText: response.statusText,\n body: errorText,\n }\n );\n }\n\n const result = (await response.json()) as {\n data?: T;\n errors?: Array<{ message: string }>;\n };\n\n if (result.errors) {\n // Check for rate limit errors in GraphQL response\n const rateLimitError = result.errors.find(\n (e) =>\n e.message.toLowerCase().includes('rate limit') ||\n e.message.toLowerCase().includes('usage limit')\n );\n\n if (rateLimitError && retries > 0) {\n const waitTime = 60000; // Default 60s wait for GraphQL rate limit errors\n logger.warn(\n `GraphQL rate limit error, retrying in ${waitTime / 1000}s (${retries} retries left)`\n );\n this.rateLimitState.retryAfter = Date.now() + waitTime;\n await this.sleep(waitTime);\n return this.graphql<T>(query, variables, retries - 1);\n }\n\n logger.error('Linear GraphQL errors:', { errors: result.errors });\n throw new IntegrationError(\n `Linear GraphQL error: ${result.errors[0].message}`,\n ErrorCode.LINEAR_API_ERROR,\n { errors: result.errors }\n );\n }\n\n return result.data as T;\n }\n\n /**\n * Create a new issue in Linear\n */\n async createIssue(input: LinearCreateIssueInput): Promise<LinearIssue> {\n const mutation = `\n mutation CreateIssue($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n assignee {\n id\n name\n email\n }\n estimate\n labels {\n nodes {\n id\n name\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n issueCreate: {\n success: boolean;\n issue: LinearIssue;\n };\n }>(mutation, { input });\n\n if (!result.issueCreate.success) {\n throw new IntegrationError(\n 'Failed to create Linear issue',\n ErrorCode.LINEAR_API_ERROR,\n { input }\n );\n }\n\n return result.issueCreate.issue;\n }\n\n /**\n * Update an existing Linear issue\n */\n async updateIssue(\n issueId: string,\n updates: Partial<LinearCreateIssueInput> & { stateId?: string }\n ): Promise<LinearIssue> {\n const mutation = `\n mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n issueUpdate(id: $id, input: $input) {\n success\n issue {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n assignee {\n id\n name\n email\n }\n estimate\n labels {\n nodes {\n id\n name\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n issueUpdate: {\n success: boolean;\n issue: LinearIssue;\n };\n }>(mutation, { id: issueId, input: updates });\n\n if (!result.issueUpdate.success) {\n throw new IntegrationError(\n `Failed to update Linear issue ${issueId}`,\n ErrorCode.LINEAR_API_ERROR,\n { issueId, updates }\n );\n }\n\n return result.issueUpdate.issue;\n }\n\n /**\n * Get issue by ID\n */\n async getIssue(issueId: string): Promise<LinearIssue | null> {\n const query = `\n query GetIssue($id: String!) {\n issue(id: $id) {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n assignee {\n id\n name\n email\n }\n estimate\n labels {\n nodes {\n id\n name\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n `;\n\n const result = await this.graphql<{\n issue: LinearIssue | null;\n }>(query, { id: issueId });\n\n return result.issue;\n }\n\n /**\n * Search for issues by identifier (e.g., \"SM-123\")\n */\n async findIssueByIdentifier(identifier: string): Promise<LinearIssue | null> {\n const query = `\n query FindIssue($filter: IssueFilter!) {\n issues(filter: $filter, first: 1) {\n nodes {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n assignee {\n id\n name\n email\n }\n estimate\n labels {\n nodes {\n id\n name\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n issues: {\n nodes: LinearIssue[];\n };\n }>(query, {\n filter: {\n number: {\n eq: parseInt(identifier.split('-')[1] || '0') || 0,\n },\n },\n });\n\n return result.issues.nodes[0] || null;\n }\n\n /**\n * Get team information\n */\n async getTeam(\n teamId?: string\n ): Promise<{ id: string; name: string; key: string }> {\n const query = teamId\n ? `\n query GetTeam($id: String!) {\n team(id: $id) {\n id\n name\n key\n }\n }\n `\n : `\n query GetTeams {\n teams(first: 1) {\n nodes {\n id\n name\n key\n }\n }\n }\n `;\n\n if (teamId) {\n const result = await this.graphql<{\n team: { id: string; name: string; key: string };\n }>(query, { id: teamId });\n if (!result.team) {\n throw new IntegrationError(\n `Team ${teamId} not found`,\n ErrorCode.LINEAR_API_ERROR,\n { teamId }\n );\n }\n return result.team;\n } else {\n const result = await this.graphql<{\n teams: {\n nodes: Array<{ id: string; name: string; key: string }>;\n };\n }>(query);\n\n if (result.teams.nodes.length === 0) {\n throw new IntegrationError(\n 'No teams found',\n ErrorCode.LINEAR_API_ERROR\n );\n }\n\n return result.teams.nodes[0]!;\n }\n }\n\n /**\n * Get workflow states for a team\n */\n async getWorkflowStates(teamId: string): Promise<\n Array<{\n id: string;\n name: string;\n type: 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled';\n color: string;\n }>\n > {\n const query = `\n query GetWorkflowStates($teamId: String!) {\n team(id: $teamId) {\n states {\n nodes {\n id\n name\n type\n color\n }\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n team: {\n states: {\n nodes: Array<{\n id: string;\n name: string;\n type:\n | 'backlog'\n | 'unstarted'\n | 'started'\n | 'completed'\n | 'cancelled';\n color: string;\n }>;\n };\n };\n }>(query, { teamId });\n\n return result.team.states.nodes;\n }\n\n /**\n * Get current viewer/user information\n */\n async getViewer(): Promise<{\n id: string;\n name: string;\n email: string;\n }> {\n const query = `\n query GetViewer {\n viewer {\n id\n name\n email\n }\n }\n `;\n\n const result = await this.graphql<{\n viewer: {\n id: string;\n name: string;\n email: string;\n };\n }>(query);\n\n return result.viewer;\n }\n\n /**\n * Get all teams for the organization\n */\n async getTeams(): Promise<\n Array<{\n id: string;\n name: string;\n key: string;\n }>\n > {\n const query = `\n query GetTeams {\n teams(first: 50) {\n nodes {\n id\n name\n key\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n teams: {\n nodes: Array<{\n id: string;\n name: string;\n key: string;\n }>;\n };\n }>(query);\n\n return result.teams.nodes;\n }\n\n /**\n * Get issues with filtering options\n */\n async getIssues(options?: {\n teamId?: string;\n assigneeId?: string;\n stateType?: 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled';\n limit?: number;\n }): Promise<LinearIssue[]> {\n const query = `\n query GetIssues($filter: IssueFilter, $first: Int!) {\n issues(filter: $filter, first: $first) {\n nodes {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n assignee {\n id\n name\n email\n }\n estimate\n labels {\n nodes {\n id\n name\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const filter: Record<string, unknown> = {};\n\n if (options?.teamId) {\n filter.team = { id: { eq: options.teamId } };\n }\n\n if (options?.assigneeId) {\n filter.assignee = { id: { eq: options.assigneeId } };\n }\n\n if (options?.stateType) {\n filter.state = { type: { eq: options.stateType } };\n }\n\n const result = await this.graphql<{\n issues: {\n nodes: LinearIssue[];\n };\n }>(query, {\n filter: Object.keys(filter).length > 0 ? filter : undefined,\n first: options?.limit || 50,\n });\n\n return result.issues.nodes;\n }\n\n /**\n * Assign an issue to a user\n */\n async assignIssue(\n issueId: string,\n assigneeId: string\n ): Promise<{ success: boolean; issue?: LinearIssue }> {\n const mutation = `\n mutation AssignIssue($issueId: String!, $assigneeId: String!) {\n issueUpdate(id: $issueId, input: { assigneeId: $assigneeId }) {\n success\n issue {\n id\n identifier\n title\n assignee {\n id\n name\n }\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n issueUpdate: {\n success: boolean;\n issue?: LinearIssue;\n };\n }>(mutation, { issueId, assigneeId });\n\n return result.issueUpdate;\n }\n\n /**\n * Update issue state (e.g., move to \"In Progress\")\n */\n async updateIssueState(\n issueId: string,\n stateId: string\n ): Promise<{ success: boolean; issue?: LinearIssue }> {\n const mutation = `\n mutation UpdateIssueState($issueId: String!, $stateId: String!) {\n issueUpdate(id: $issueId, input: { stateId: $stateId }) {\n success\n issue {\n id\n identifier\n title\n state {\n id\n name\n type\n }\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n issueUpdate: {\n success: boolean;\n issue?: LinearIssue;\n };\n }>(mutation, { issueId, stateId });\n\n return result.issueUpdate;\n }\n\n /**\n * Get an issue by ID with team info\n */\n async getIssueById(issueId: string): Promise<LinearIssue | null> {\n const query = `\n query GetIssue($issueId: String!) {\n issue(id: $issueId) {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n assignee {\n id\n name\n email\n }\n estimate\n labels {\n nodes {\n id\n name\n }\n }\n team {\n id\n name\n }\n createdAt\n updatedAt\n url\n }\n }\n `;\n\n try {\n const result = await this.graphql<{\n issue: LinearIssue & { team: { id: string; name: string } };\n }>(query, { issueId });\n return result.issue;\n } catch {\n return null;\n }\n }\n\n /**\n * Start working on an issue (assign to self and move to In Progress)\n */\n async startIssue(issueId: string): Promise<{\n success: boolean;\n issue?: LinearIssue;\n error?: string;\n }> {\n try {\n // Get current user\n const user = await this.getViewer();\n\n // Get the issue to find its team\n const issue = await this.getIssueById(issueId);\n if (!issue) {\n return { success: false, error: 'Issue not found' };\n }\n\n // Assign to self\n const assignResult = await this.assignIssue(issueId, user.id);\n if (!assignResult.success) {\n return { success: false, error: 'Failed to assign issue' };\n }\n\n // Find the \"In Progress\" or \"started\" state for this issue's team\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const teamId = (issue as any).team?.id;\n if (teamId) {\n const states = await this.getWorkflowStates(teamId);\n const inProgressState = states.find(\n (s) =>\n s.type === 'started' || s.name.toLowerCase().includes('progress')\n );\n\n if (inProgressState) {\n await this.updateIssueState(issueId, inProgressState.id);\n }\n }\n\n return { success: true, issue: assignResult.issue };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Linear API Client for StackMemory\n * Handles bi-directional sync with Linear's GraphQL API\n */\n\nimport { logger } from '../../core/monitoring/logger.js';\nimport { IntegrationError, ErrorCode } from '../../core/errors/index.js';\n\nexport interface LinearConfig {\n apiKey: string;\n teamId?: string;\n webhookSecret?: string;\n baseUrl?: string;\n // If true, send Authorization header as `Bearer <apiKey>` (OAuth access token)\n useBearer?: boolean;\n // Optional callback to refresh token on 401 and return the new access token\n onUnauthorized?: () => Promise<string>;\n}\n\nexport interface LinearIssue {\n id: string;\n identifier: string; // Like \"SM-123\"\n title: string;\n description?: string;\n state: {\n id: string;\n name: string;\n type: 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled';\n };\n priority: number; // 0-4 (0=none, 1=urgent, 2=high, 3=medium, 4=low)\n assignee?: {\n id: string;\n name: string;\n email: string;\n };\n estimate?: number; // Story points\n labels: Array<{\n id: string;\n name: string;\n }>;\n createdAt: string;\n updatedAt: string;\n url: string;\n}\n\nexport interface LinearCreateIssueInput {\n title: string;\n description?: string;\n teamId: string;\n priority?: number;\n estimate?: number;\n labelIds?: string[];\n}\n\ninterface RateLimitState {\n remaining: number;\n resetAt: number;\n retryAfter: number;\n}\n\nexport class LinearClient {\n private config: LinearConfig;\n private baseUrl: string;\n private rateLimitState: RateLimitState = {\n remaining: 1500, // Linear's default limit\n resetAt: Date.now() + 3600000,\n retryAfter: 0,\n };\n private requestQueue: Array<() => Promise<void>> = [];\n private isProcessingQueue = false;\n private minRequestInterval = 100; // Minimum ms between requests\n private lastRequestTime = 0;\n\n constructor(config: LinearConfig) {\n this.config = config;\n this.baseUrl = config.baseUrl || 'https://api.linear.app';\n\n if (!config.apiKey) {\n throw new IntegrationError(\n 'Linear API key is required',\n ErrorCode.LINEAR_AUTH_FAILED\n );\n }\n }\n\n /**\n * Wait for rate limit to reset if needed\n */\n private async waitForRateLimit(): Promise<void> {\n const now = Date.now();\n\n // Check if we're in a retry-after period\n if (this.rateLimitState.retryAfter > now) {\n const waitTime = this.rateLimitState.retryAfter - now;\n logger.warn(`Rate limited, waiting ${Math.ceil(waitTime / 1000)}s`);\n await this.sleep(waitTime);\n }\n\n // Check if we've exhausted our rate limit\n if (this.rateLimitState.remaining <= 5) {\n if (this.rateLimitState.resetAt > now) {\n const waitTime = this.rateLimitState.resetAt - now;\n logger.warn(\n `Rate limit nearly exhausted, waiting ${Math.ceil(waitTime / 1000)}s for reset`\n );\n await this.sleep(Math.min(waitTime, 60000)); // Max 60s wait\n }\n }\n\n // Ensure minimum interval between requests\n const timeSinceLastRequest = now - this.lastRequestTime;\n if (timeSinceLastRequest < this.minRequestInterval) {\n await this.sleep(this.minRequestInterval - timeSinceLastRequest);\n }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n /**\n * Update rate limit state from response headers\n */\n private updateRateLimitState(response: Response): void {\n const remaining = response.headers.get('x-ratelimit-remaining');\n const reset = response.headers.get('x-ratelimit-reset');\n const retryAfter = response.headers.get('retry-after');\n\n if (remaining !== null) {\n this.rateLimitState.remaining = parseInt(remaining, 10);\n }\n if (reset !== null) {\n this.rateLimitState.resetAt = parseInt(reset, 10) * 1000;\n }\n if (retryAfter !== null) {\n this.rateLimitState.retryAfter =\n Date.now() + parseInt(retryAfter, 10) * 1000;\n }\n }\n\n /**\n * Execute GraphQL query against Linear API with rate limiting\n */\n private async graphql<T>(\n query: string,\n variables?: Record<string, unknown>,\n retries = 3,\n allowAuthRefresh = true\n ): Promise<T> {\n // Wait for rate limit before making request\n await this.waitForRateLimit();\n\n this.lastRequestTime = Date.now();\n\n const authHeader = this.config.useBearer\n ? `Bearer ${this.config.apiKey}`\n : this.config.apiKey;\n\n let response = await fetch(`${this.baseUrl}/graphql`, {\n method: 'POST',\n headers: {\n Authorization: authHeader,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n query,\n variables,\n }),\n });\n\n // Update rate limit state from response\n this.updateRateLimitState(response);\n\n // Handle unauthorized (e.g., expired OAuth token)\n if (\n response.status === 401 &&\n this.config.onUnauthorized &&\n allowAuthRefresh\n ) {\n try {\n const newToken = await this.config.onUnauthorized();\n // Update local config and retry once without further auth refresh\n this.config.apiKey = newToken;\n const retryHeader = this.config.useBearer\n ? `Bearer ${newToken}`\n : newToken;\n response = await fetch(`${this.baseUrl}/graphql`, {\n method: 'POST',\n headers: {\n Authorization: retryHeader,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ query, variables }),\n });\n this.updateRateLimitState(response);\n } catch (e: unknown) {\n // Fall through to standard error handling\n }\n }\n\n // Handle rate limiting with exponential backoff\n if (response.status === 429) {\n if (retries > 0) {\n const retryAfter = response.headers.get('retry-after');\n const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : 60000;\n logger.warn(\n `Rate limited (429), retrying in ${waitTime / 1000}s (${retries} retries left)`\n );\n this.rateLimitState.retryAfter = Date.now() + waitTime;\n await this.sleep(waitTime);\n return this.graphql<T>(query, variables, retries - 1, allowAuthRefresh);\n }\n throw new IntegrationError(\n 'Linear API rate limit exceeded after retries',\n ErrorCode.LINEAR_API_ERROR,\n { retries: 0 }\n );\n }\n\n if (!response.ok) {\n const errorText = await response.text();\n // Don't log 401/403 - expected when not authenticated\n if (response.status !== 401 && response.status !== 403) {\n logger.error(\n 'Linear API error response:',\n new Error(`${response.status}: ${errorText}`)\n );\n }\n throw new IntegrationError(\n `Linear API error: ${response.status} ${response.statusText}`,\n ErrorCode.LINEAR_API_ERROR,\n {\n status: response.status,\n statusText: response.statusText,\n body: errorText,\n }\n );\n }\n\n const result = (await response.json()) as {\n data?: T;\n errors?: Array<{ message: string }>;\n };\n\n if (result.errors) {\n // Check for rate limit errors in GraphQL response\n const rateLimitError = result.errors.find(\n (e) =>\n e.message.toLowerCase().includes('rate limit') ||\n e.message.toLowerCase().includes('usage limit')\n );\n\n if (rateLimitError && retries > 0) {\n const waitTime = 60000; // Default 60s wait for GraphQL rate limit errors\n logger.warn(\n `GraphQL rate limit error, retrying in ${waitTime / 1000}s (${retries} retries left)`\n );\n this.rateLimitState.retryAfter = Date.now() + waitTime;\n await this.sleep(waitTime);\n return this.graphql<T>(query, variables, retries - 1);\n }\n\n logger.error('Linear GraphQL errors:', { errors: result.errors });\n throw new IntegrationError(\n `Linear GraphQL error: ${result.errors[0].message}`,\n ErrorCode.LINEAR_API_ERROR,\n { errors: result.errors }\n );\n }\n\n return result.data as T;\n }\n\n /**\n * Create a new issue in Linear\n */\n async createIssue(input: LinearCreateIssueInput): Promise<LinearIssue> {\n const mutation = `\n mutation CreateIssue($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n assignee {\n id\n name\n email\n }\n estimate\n labels {\n nodes {\n id\n name\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n issueCreate: {\n success: boolean;\n issue: LinearIssue;\n };\n }>(mutation, { input });\n\n if (!result.issueCreate.success) {\n throw new IntegrationError(\n 'Failed to create Linear issue',\n ErrorCode.LINEAR_API_ERROR,\n { input }\n );\n }\n\n return result.issueCreate.issue;\n }\n\n /**\n * Update an existing Linear issue\n */\n async updateIssue(\n issueId: string,\n updates: Partial<LinearCreateIssueInput> & { stateId?: string }\n ): Promise<LinearIssue> {\n const mutation = `\n mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n issueUpdate(id: $id, input: $input) {\n success\n issue {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n assignee {\n id\n name\n email\n }\n estimate\n labels {\n nodes {\n id\n name\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n issueUpdate: {\n success: boolean;\n issue: LinearIssue;\n };\n }>(mutation, { id: issueId, input: updates });\n\n if (!result.issueUpdate.success) {\n throw new IntegrationError(\n `Failed to update Linear issue ${issueId}`,\n ErrorCode.LINEAR_API_ERROR,\n { issueId, updates }\n );\n }\n\n return result.issueUpdate.issue;\n }\n\n /**\n * Get issue by ID\n */\n async getIssue(issueId: string): Promise<LinearIssue | null> {\n const query = `\n query GetIssue($id: String!) {\n issue(id: $id) {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n assignee {\n id\n name\n email\n }\n estimate\n labels {\n nodes {\n id\n name\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n `;\n\n const result = await this.graphql<{\n issue: LinearIssue | null;\n }>(query, { id: issueId });\n\n return result.issue;\n }\n\n /**\n * Search for issues by identifier (e.g., \"SM-123\")\n */\n async findIssueByIdentifier(identifier: string): Promise<LinearIssue | null> {\n const query = `\n query FindIssue($filter: IssueFilter!) {\n issues(filter: $filter, first: 1) {\n nodes {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n assignee {\n id\n name\n email\n }\n estimate\n labels {\n nodes {\n id\n name\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n issues: {\n nodes: LinearIssue[];\n };\n }>(query, {\n filter: {\n number: {\n eq: parseInt(identifier.split('-')[1] || '0') || 0,\n },\n },\n });\n\n return result.issues.nodes[0] || null;\n }\n\n /**\n * Get team information\n */\n async getTeam(\n teamId?: string\n ): Promise<{ id: string; name: string; key: string }> {\n const query = teamId\n ? `\n query GetTeam($id: String!) {\n team(id: $id) {\n id\n name\n key\n }\n }\n `\n : `\n query GetTeams {\n teams(first: 1) {\n nodes {\n id\n name\n key\n }\n }\n }\n `;\n\n if (teamId) {\n const result = await this.graphql<{\n team: { id: string; name: string; key: string };\n }>(query, { id: teamId });\n if (!result.team) {\n throw new IntegrationError(\n `Team ${teamId} not found`,\n ErrorCode.LINEAR_API_ERROR,\n { teamId }\n );\n }\n return result.team;\n } else {\n const result = await this.graphql<{\n teams: {\n nodes: Array<{ id: string; name: string; key: string }>;\n };\n }>(query);\n\n if (result.teams.nodes.length === 0) {\n throw new IntegrationError(\n 'No teams found',\n ErrorCode.LINEAR_API_ERROR\n );\n }\n\n return result.teams.nodes[0]!;\n }\n }\n\n /**\n * Get workflow states for a team\n */\n async getWorkflowStates(teamId: string): Promise<\n Array<{\n id: string;\n name: string;\n type: 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled';\n color: string;\n }>\n > {\n const query = `\n query GetWorkflowStates($teamId: String!) {\n team(id: $teamId) {\n states {\n nodes {\n id\n name\n type\n color\n }\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n team: {\n states: {\n nodes: Array<{\n id: string;\n name: string;\n type:\n | 'backlog'\n | 'unstarted'\n | 'started'\n | 'completed'\n | 'cancelled';\n color: string;\n }>;\n };\n };\n }>(query, { teamId });\n\n return result.team.states.nodes;\n }\n\n /**\n * Get current viewer/user information\n */\n async getViewer(): Promise<{\n id: string;\n name: string;\n email: string;\n }> {\n const query = `\n query GetViewer {\n viewer {\n id\n name\n email\n }\n }\n `;\n\n const result = await this.graphql<{\n viewer: {\n id: string;\n name: string;\n email: string;\n };\n }>(query);\n\n return result.viewer;\n }\n\n /**\n * Get all teams for the organization\n */\n async getTeams(): Promise<\n Array<{\n id: string;\n name: string;\n key: string;\n }>\n > {\n const query = `\n query GetTeams {\n teams(first: 50) {\n nodes {\n id\n name\n key\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n teams: {\n nodes: Array<{\n id: string;\n name: string;\n key: string;\n }>;\n };\n }>(query);\n\n return result.teams.nodes;\n }\n\n /**\n * Get issues with filtering options\n */\n async getIssues(options?: {\n teamId?: string;\n assigneeId?: string;\n stateType?: 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled';\n limit?: number;\n }): Promise<LinearIssue[]> {\n const query = `\n query GetIssues($filter: IssueFilter, $first: Int!) {\n issues(filter: $filter, first: $first) {\n nodes {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n assignee {\n id\n name\n email\n }\n estimate\n labels {\n nodes {\n id\n name\n }\n }\n createdAt\n updatedAt\n url\n }\n }\n }\n `;\n\n const filter: Record<string, unknown> = {};\n\n if (options?.teamId) {\n filter.team = { id: { eq: options.teamId } };\n }\n\n if (options?.assigneeId) {\n filter.assignee = { id: { eq: options.assigneeId } };\n }\n\n if (options?.stateType) {\n filter.state = { type: { eq: options.stateType } };\n }\n\n const result = await this.graphql<{\n issues: {\n nodes: LinearIssue[];\n };\n }>(query, {\n filter: Object.keys(filter).length > 0 ? filter : undefined,\n first: options?.limit || 50,\n });\n\n return result.issues.nodes;\n }\n\n /**\n * Assign an issue to a user\n */\n async assignIssue(\n issueId: string,\n assigneeId: string\n ): Promise<{ success: boolean; issue?: LinearIssue }> {\n const mutation = `\n mutation AssignIssue($issueId: String!, $assigneeId: String!) {\n issueUpdate(id: $issueId, input: { assigneeId: $assigneeId }) {\n success\n issue {\n id\n identifier\n title\n assignee {\n id\n name\n }\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n issueUpdate: {\n success: boolean;\n issue?: LinearIssue;\n };\n }>(mutation, { issueId, assigneeId });\n\n return result.issueUpdate;\n }\n\n /**\n * Update issue state (e.g., move to \"In Progress\")\n */\n async updateIssueState(\n issueId: string,\n stateId: string\n ): Promise<{ success: boolean; issue?: LinearIssue }> {\n const mutation = `\n mutation UpdateIssueState($issueId: String!, $stateId: String!) {\n issueUpdate(id: $issueId, input: { stateId: $stateId }) {\n success\n issue {\n id\n identifier\n title\n state {\n id\n name\n type\n }\n }\n }\n }\n `;\n\n const result = await this.graphql<{\n issueUpdate: {\n success: boolean;\n issue?: LinearIssue;\n };\n }>(mutation, { issueId, stateId });\n\n return result.issueUpdate;\n }\n\n /**\n * Get an issue by ID with team info\n */\n async getIssueById(issueId: string): Promise<LinearIssue | null> {\n const query = `\n query GetIssue($issueId: String!) {\n issue(id: $issueId) {\n id\n identifier\n title\n description\n state {\n id\n name\n type\n }\n priority\n assignee {\n id\n name\n email\n }\n estimate\n labels {\n nodes {\n id\n name\n }\n }\n team {\n id\n name\n }\n createdAt\n updatedAt\n url\n }\n }\n `;\n\n try {\n const result = await this.graphql<{\n issue: LinearIssue & { team: { id: string; name: string } };\n }>(query, { issueId });\n return result.issue;\n } catch {\n return null;\n }\n }\n\n /**\n * Start working on an issue (assign to self and move to In Progress)\n */\n async startIssue(issueId: string): Promise<{\n success: boolean;\n issue?: LinearIssue;\n error?: string;\n }> {\n try {\n // Get current user\n const user = await this.getViewer();\n\n // Get the issue to find its team\n const issue = await this.getIssueById(issueId);\n if (!issue) {\n return { success: false, error: 'Issue not found' };\n }\n\n // Assign to self\n const assignResult = await this.assignIssue(issueId, user.id);\n if (!assignResult.success) {\n return { success: false, error: 'Failed to assign issue' };\n }\n\n // Find the \"In Progress\" or \"started\" state for this issue's team\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const teamId = (issue as any).team?.id;\n if (teamId) {\n const states = await this.getWorkflowStates(teamId);\n const inProgressState = states.find(\n (s) =>\n s.type === 'started' || s.name.toLowerCase().includes('progress')\n );\n\n if (inProgressState) {\n await this.updateIssueState(issueId, inProgressState.id);\n }\n }\n\n return { success: true, issue: assignResult.issue };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "0.5.40",
3
+ "version": "0.5.42",
4
4
  "description": "Lossless memory runtime for AI coding tools - organizes context as a call stack instead of linear chat logs, with team collaboration and infinite retention",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",