@stackmemoryai/stackmemory 0.5.28 → 0.5.30
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 +37 -16
- package/dist/cli/claude-sm.js +17 -5
- package/dist/cli/claude-sm.js.map +2 -2
- package/dist/core/database/batch-operations.js +29 -4
- package/dist/core/database/batch-operations.js.map +2 -2
- package/dist/core/database/connection-pool.js +13 -2
- package/dist/core/database/connection-pool.js.map +2 -2
- package/dist/core/database/migration-manager.js +130 -34
- package/dist/core/database/migration-manager.js.map +2 -2
- package/dist/core/database/paradedb-adapter.js +23 -7
- package/dist/core/database/paradedb-adapter.js.map +2 -2
- package/dist/core/database/query-router.js +8 -3
- package/dist/core/database/query-router.js.map +2 -2
- package/dist/core/database/sqlite-adapter.js +152 -33
- package/dist/core/database/sqlite-adapter.js.map +2 -2
- package/dist/hooks/session-summary.js +23 -2
- package/dist/hooks/session-summary.js.map +2 -2
- package/dist/hooks/sms-notify.js +47 -13
- package/dist/hooks/sms-notify.js.map +2 -2
- package/dist/integrations/linear/auth.js +34 -20
- package/dist/integrations/linear/auth.js.map +2 -2
- package/dist/integrations/linear/auto-sync.js +18 -8
- package/dist/integrations/linear/auto-sync.js.map +2 -2
- package/dist/integrations/linear/client.js +42 -9
- package/dist/integrations/linear/client.js.map +2 -2
- package/dist/integrations/linear/migration.js +94 -36
- package/dist/integrations/linear/migration.js.map +2 -2
- package/dist/integrations/linear/oauth-server.js +77 -34
- package/dist/integrations/linear/oauth-server.js.map +2 -2
- package/dist/integrations/linear/rest-client.js +13 -3
- package/dist/integrations/linear/rest-client.js.map +2 -2
- package/dist/integrations/linear/sync-service.js +18 -15
- package/dist/integrations/linear/sync-service.js.map +2 -2
- package/dist/integrations/linear/sync.js +12 -4
- package/dist/integrations/linear/sync.js.map +2 -2
- package/dist/integrations/linear/unified-sync.js +33 -8
- package/dist/integrations/linear/unified-sync.js.map +2 -2
- package/dist/integrations/linear/webhook-handler.js +5 -1
- package/dist/integrations/linear/webhook-handler.js.map +2 -2
- package/dist/integrations/linear/webhook-server.js +7 -7
- package/dist/integrations/linear/webhook-server.js.map +2 -2
- package/dist/integrations/linear/webhook.js +9 -2
- package/dist/integrations/linear/webhook.js.map +2 -2
- package/dist/integrations/mcp/schemas.js +147 -0
- package/dist/integrations/mcp/schemas.js.map +7 -0
- package/dist/integrations/mcp/server.js +19 -3
- package/dist/integrations/mcp/server.js.map +2 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/session-summary.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Session Summary Generator\n * Generates intelligent suggestions for what to do next after a Claude session\n */\n\nimport { execSync } from 'child_process';\nimport { pickNextLinearTask, TaskSuggestion } from './linear-task-picker.js';\n\nexport interface SessionContext {\n instanceId: string;\n exitCode: number | null;\n sessionStartTime: number;\n worktreePath?: string;\n branch?: string;\n task?: string;\n}\n\nexport interface Suggestion {\n key: string;\n label: string;\n action: string;\n priority: number;\n}\n\nexport interface SessionSummary {\n duration: string;\n exitCode: number | null;\n branch: string;\n status: 'success' | 'error' | 'interrupted';\n suggestions: Suggestion[];\n linearTask?: TaskSuggestion;\n}\n\n/**\n * Format duration in human-readable form\n */\nfunction formatDuration(ms: number): string {\n const seconds = Math.floor(ms / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n\n if (hours > 0) {\n return `${hours}h ${minutes % 60}min`;\n }\n if (minutes > 0) {\n return `${minutes}min`;\n }\n return `${seconds}s`;\n}\n\n/**\n * Get current git branch\n */\nfunction getCurrentBranch(): string {\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Check for uncommitted changes\n */\nfunction hasUncommittedChanges(): { changed: boolean; count: number } {\n try {\n const status = execSync('git status --porcelain', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n const lines = status.trim().split('\\n').filter(Boolean);\n return { changed: lines.length > 0, count: lines.length };\n } catch {\n return { changed: false, count: 0 };\n }\n}\n\n/**\n * Check if we're in a worktree\n */\nfunction isInWorktree(): boolean {\n try {\n execSync('git rev-parse --is-inside-work-tree', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n // Check if it's a worktree (not the main repo)\n const gitDir = execSync('git rev-parse --git-dir', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n return gitDir.includes('.git/worktrees/');\n } catch {\n return false;\n }\n}\n\n/**\n * Check if tests exist and might need running\n */\nfunction hasTestScript(): boolean {\n try {\n const packageJson = execSync('cat package.json', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n const pkg = JSON.parse(packageJson);\n return !!(pkg.scripts?.test || pkg.scripts?.['test:run']);\n } catch {\n return false;\n }\n}\n\n/**\n * Generate suggestions based on session context\n */\nasync function generateSuggestions(\n context: SessionContext\n): Promise<Suggestion[]> {\n const suggestions: Suggestion[] = [];\n let keyIndex = 1;\n\n const changes = hasUncommittedChanges();\n const inWorktree = isInWorktree();\n const hasTests = hasTestScript();\n\n // Error case - suggest reviewing logs\n if (context.exitCode !== 0 && context.exitCode !== null) {\n suggestions.push({\n key: String(keyIndex++),\n label: 'Review error logs',\n action: 'cat ~/.claude/logs/claude-*.log | tail -50',\n priority: 100,\n });\n }\n\n // Uncommitted changes - suggest commit or PR\n if (changes.changed) {\n suggestions.push({\n key: String(keyIndex++),\n label: `Commit changes (${changes.count} files)`,\n action: 'git add -A && git commit',\n priority: 90,\n });\n\n // If on feature branch, suggest PR\n const branch = getCurrentBranch();\n if (branch !== 'main' && branch !== 'master' && branch !== 'unknown') {\n suggestions.push({\n key: String(keyIndex++),\n label: 'Create PR',\n action: 'gh pr create --fill',\n priority: 80,\n });\n }\n }\n\n // If tests exist and changes were made, suggest running tests\n if (hasTests && changes.changed) {\n suggestions.push({\n key: String(keyIndex++),\n label: 'Run tests',\n action: 'npm run test:run',\n priority: 85,\n });\n }\n\n // Worktree-specific suggestions\n if (inWorktree) {\n suggestions.push({\n key: String(keyIndex++),\n label: 'Merge to main',\n action: 'cwm', // custom alias\n priority: 70,\n });\n }\n\n // Try to get next Linear task\n try {\n const linearTask = await pickNextLinearTask({ preferTestTasks: true });\n if (linearTask) {\n suggestions.push({\n key: String(keyIndex++),\n label: `Start: ${linearTask.identifier} - ${linearTask.title.substring(0, 40)}${linearTask.title.length > 40 ? '...' : ''}${linearTask.hasTestRequirements ? ' (has tests)' : ''}`,\n action: `stackmemory task start ${linearTask.id} --assign-me`,\n priority: 60,\n });\n }\n } catch {\n // Linear not available, skip\n }\n\n // Long session suggestion\n const durationMs = Date.now() - context.sessionStartTime;\n if (durationMs > 30 * 60 * 1000) {\n // > 30 minutes\n suggestions.push({\n key: String(keyIndex++),\n label: 'Take a break',\n action: 'echo \"Great work! Time for a coffee break.\"',\n priority: 10,\n });\n }\n\n // Sort by priority (highest first) and re-key\n suggestions.sort((a, b) => b.priority - a.priority);\n suggestions.forEach((s, i) => {\n s.key = String(i + 1);\n });\n\n return suggestions;\n}\n\n/**\n * Generate full session summary\n */\nexport async function generateSessionSummary(\n context: SessionContext\n): Promise<SessionSummary> {\n const durationMs = Date.now() - context.sessionStartTime;\n const duration = formatDuration(durationMs);\n const branch = context.branch || getCurrentBranch();\n\n let status: 'success' | 'error' | 'interrupted' = 'success';\n if (context.exitCode !== 0 && context.exitCode !== null) {\n status = 'error';\n }\n\n const suggestions = await generateSuggestions(context);\n\n // Extract linear task if present\n let linearTask: TaskSuggestion | undefined;\n try {\n linearTask = await pickNextLinearTask({ preferTestTasks: true });\n } catch {\n // Linear not available\n }\n\n return {\n duration,\n exitCode: context.exitCode,\n branch,\n status,\n suggestions,\n linearTask,\n };\n}\n\n/**\n * Format session summary as WhatsApp message\n */\nexport function formatSummaryMessage(summary: SessionSummary): string {\n const statusEmoji = summary.status === 'success' ? '' : '';\n const exitInfo =\n summary.exitCode !== null ? ` | Exit: ${summary.exitCode}` : '';\n\n let message = `Claude session complete ${statusEmoji}\\n`;\n message += `Duration: ${summary.duration}${exitInfo}
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Session Summary Generator\n * Generates intelligent suggestions for what to do next after a Claude session\n */\n\nimport { execSync } from 'child_process';\nimport { pickNextLinearTask, TaskSuggestion } from './linear-task-picker.js';\n\nexport interface SessionContext {\n instanceId: string;\n exitCode: number | null;\n sessionStartTime: number;\n worktreePath?: string;\n branch?: string;\n task?: string;\n}\n\nexport interface Suggestion {\n key: string;\n label: string;\n action: string;\n priority: number;\n}\n\nexport interface SessionSummary {\n duration: string;\n exitCode: number | null;\n branch: string;\n status: 'success' | 'error' | 'interrupted';\n suggestions: Suggestion[];\n linearTask?: TaskSuggestion;\n}\n\n/**\n * Format duration in human-readable form\n */\nfunction formatDuration(ms: number): string {\n const seconds = Math.floor(ms / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n\n if (hours > 0) {\n return `${hours}h ${minutes % 60}min`;\n }\n if (minutes > 0) {\n return `${minutes}min`;\n }\n return `${seconds}s`;\n}\n\n/**\n * Get current git branch\n */\nfunction getCurrentBranch(): string {\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Check for uncommitted changes\n */\nfunction hasUncommittedChanges(): { changed: boolean; count: number } {\n try {\n const status = execSync('git status --porcelain', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n const lines = status.trim().split('\\n').filter(Boolean);\n return { changed: lines.length > 0, count: lines.length };\n } catch {\n return { changed: false, count: 0 };\n }\n}\n\n/**\n * Check if we're in a worktree\n */\nfunction isInWorktree(): boolean {\n try {\n execSync('git rev-parse --is-inside-work-tree', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n // Check if it's a worktree (not the main repo)\n const gitDir = execSync('git rev-parse --git-dir', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim();\n return gitDir.includes('.git/worktrees/');\n } catch {\n return false;\n }\n}\n\n/**\n * Check if tests exist and might need running\n */\nfunction hasTestScript(): boolean {\n try {\n const packageJson = execSync('cat package.json', {\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n const pkg = JSON.parse(packageJson);\n return !!(pkg.scripts?.test || pkg.scripts?.['test:run']);\n } catch {\n return false;\n }\n}\n\n/**\n * Generate suggestions based on session context\n */\nasync function generateSuggestions(\n context: SessionContext\n): Promise<Suggestion[]> {\n const suggestions: Suggestion[] = [];\n let keyIndex = 1;\n\n const changes = hasUncommittedChanges();\n const inWorktree = isInWorktree();\n const hasTests = hasTestScript();\n\n // Error case - suggest reviewing logs\n if (context.exitCode !== 0 && context.exitCode !== null) {\n suggestions.push({\n key: String(keyIndex++),\n label: 'Review error logs',\n action: 'cat ~/.claude/logs/claude-*.log | tail -50',\n priority: 100,\n });\n }\n\n // Uncommitted changes - suggest commit or PR\n if (changes.changed) {\n suggestions.push({\n key: String(keyIndex++),\n label: `Commit changes (${changes.count} files)`,\n action: 'git add -A && git commit',\n priority: 90,\n });\n\n // If on feature branch, suggest PR\n const branch = getCurrentBranch();\n if (branch !== 'main' && branch !== 'master' && branch !== 'unknown') {\n suggestions.push({\n key: String(keyIndex++),\n label: 'Create PR',\n action: 'gh pr create --fill',\n priority: 80,\n });\n }\n }\n\n // If tests exist and changes were made, suggest running tests\n if (hasTests && changes.changed) {\n suggestions.push({\n key: String(keyIndex++),\n label: 'Run tests',\n action: 'npm run test:run',\n priority: 85,\n });\n }\n\n // Worktree-specific suggestions\n if (inWorktree) {\n suggestions.push({\n key: String(keyIndex++),\n label: 'Merge to main',\n action: 'cwm', // custom alias\n priority: 70,\n });\n }\n\n // Try to get next Linear task\n try {\n const linearTask = await pickNextLinearTask({ preferTestTasks: true });\n if (linearTask) {\n suggestions.push({\n key: String(keyIndex++),\n label: `Start: ${linearTask.identifier} - ${linearTask.title.substring(0, 40)}${linearTask.title.length > 40 ? '...' : ''}${linearTask.hasTestRequirements ? ' (has tests)' : ''}`,\n action: `stackmemory task start ${linearTask.id} --assign-me`,\n priority: 60,\n });\n }\n } catch {\n // Linear not available, skip\n }\n\n // Long session suggestion\n const durationMs = Date.now() - context.sessionStartTime;\n if (durationMs > 30 * 60 * 1000) {\n // > 30 minutes\n suggestions.push({\n key: String(keyIndex++),\n label: 'Take a break',\n action: 'echo \"Great work! Time for a coffee break.\"',\n priority: 10,\n });\n }\n\n // Sort by priority (highest first) and re-key\n suggestions.sort((a, b) => b.priority - a.priority);\n\n // Ensure minimum 2 options always\n if (suggestions.length < 2) {\n // Add default options if not enough suggestions\n if (suggestions.length === 0) {\n suggestions.push({\n key: '1',\n label: 'Start new Claude session',\n action: 'claude-sm',\n priority: 50,\n });\n }\n if (suggestions.length < 2) {\n suggestions.push({\n key: '2',\n label: 'View session logs',\n action: 'cat ~/.claude/logs/claude-*.log | tail -30',\n priority: 40,\n });\n }\n }\n\n suggestions.forEach((s, i) => {\n s.key = String(i + 1);\n });\n\n return suggestions;\n}\n\n/**\n * Generate full session summary\n */\nexport async function generateSessionSummary(\n context: SessionContext\n): Promise<SessionSummary> {\n const durationMs = Date.now() - context.sessionStartTime;\n const duration = formatDuration(durationMs);\n const branch = context.branch || getCurrentBranch();\n\n let status: 'success' | 'error' | 'interrupted' = 'success';\n if (context.exitCode !== 0 && context.exitCode !== null) {\n status = 'error';\n }\n\n const suggestions = await generateSuggestions(context);\n\n // Extract linear task if present\n let linearTask: TaskSuggestion | undefined;\n try {\n linearTask = await pickNextLinearTask({ preferTestTasks: true });\n } catch {\n // Linear not available\n }\n\n return {\n duration,\n exitCode: context.exitCode,\n branch,\n status,\n suggestions,\n linearTask,\n };\n}\n\n/**\n * Format session summary as WhatsApp message\n */\nexport function formatSummaryMessage(\n summary: SessionSummary,\n sessionId?: string\n): string {\n const statusEmoji = summary.status === 'success' ? '' : '';\n const exitInfo =\n summary.exitCode !== null ? ` | Exit: ${summary.exitCode}` : '';\n const sessionInfo = sessionId ? ` | Session: ${sessionId}` : '';\n\n let message = `Claude session complete ${statusEmoji}\\n`;\n message += `Duration: ${summary.duration}${exitInfo}${sessionInfo}\\n`;\n message += `Branch: ${summary.branch}\\n\\n`;\n\n if (summary.suggestions.length > 0) {\n message += `What to do next:\\n`;\n for (const s of summary.suggestions.slice(0, 4)) {\n message += `${s.key}. ${s.label}\\n`;\n }\n message += `\\nReply with number or custom action`;\n } else {\n message += `No pending actions. Nice work!`;\n }\n\n return message;\n}\n\n/**\n * Get action for a suggestion key\n */\nexport function getActionForKey(\n suggestions: Suggestion[],\n key: string\n): string | null {\n const suggestion = suggestions.find((s) => s.key === key);\n return suggestion?.action || null;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/hooks/sms-notify.js
CHANGED
|
@@ -330,29 +330,43 @@ function processIncomingResponse(from, body) {
|
|
|
330
330
|
}
|
|
331
331
|
return { matched: false, prompt };
|
|
332
332
|
}
|
|
333
|
+
function getSessionId() {
|
|
334
|
+
return process.env["CLAUDE_INSTANCE_ID"] || process.env["STACKMEMORY_SESSION_ID"] || Math.random().toString(36).substring(2, 8);
|
|
335
|
+
}
|
|
333
336
|
async function notifyReviewReady(title, description, options) {
|
|
337
|
+
const sessionId = getSessionId();
|
|
338
|
+
let finalOptions = options || [];
|
|
339
|
+
if (finalOptions.length < 2) {
|
|
340
|
+
const defaults = [
|
|
341
|
+
{ label: "Approve", action: 'echo "Approved"' },
|
|
342
|
+
{ label: "Request changes", action: 'echo "Changes requested"' }
|
|
343
|
+
];
|
|
344
|
+
finalOptions = [...finalOptions, ...defaults].slice(
|
|
345
|
+
0,
|
|
346
|
+
Math.max(2, finalOptions.length)
|
|
347
|
+
);
|
|
348
|
+
}
|
|
334
349
|
const payload = {
|
|
335
350
|
type: "review_ready",
|
|
336
|
-
title: `Review Ready: ${title}`,
|
|
337
|
-
message: description
|
|
338
|
-
|
|
339
|
-
if (options && options.length > 0) {
|
|
340
|
-
payload.prompt = {
|
|
351
|
+
title: `[Claude ${sessionId}] Review Ready: ${title}`,
|
|
352
|
+
message: description,
|
|
353
|
+
prompt: {
|
|
341
354
|
type: "options",
|
|
342
|
-
options:
|
|
355
|
+
options: finalOptions.map((opt, i) => ({
|
|
343
356
|
key: String(i + 1),
|
|
344
357
|
label: opt.label,
|
|
345
358
|
action: opt.action
|
|
346
359
|
})),
|
|
347
360
|
question: "What would you like to do?"
|
|
348
|
-
}
|
|
349
|
-
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
350
363
|
return sendSMSNotification(payload);
|
|
351
364
|
}
|
|
352
365
|
async function notifyWithYesNo(title, question, yesAction, noAction) {
|
|
366
|
+
const sessionId = getSessionId();
|
|
353
367
|
return sendSMSNotification({
|
|
354
368
|
type: "custom",
|
|
355
|
-
title
|
|
369
|
+
title: `[Claude ${sessionId}] ${title}`,
|
|
356
370
|
message: question,
|
|
357
371
|
prompt: {
|
|
358
372
|
type: "yesno",
|
|
@@ -364,19 +378,39 @@ async function notifyWithYesNo(title, question, yesAction, noAction) {
|
|
|
364
378
|
});
|
|
365
379
|
}
|
|
366
380
|
async function notifyTaskComplete(taskName, summary) {
|
|
381
|
+
const sessionId = getSessionId();
|
|
367
382
|
return sendSMSNotification({
|
|
368
383
|
type: "task_complete",
|
|
369
|
-
title: `Task Complete: ${taskName}`,
|
|
370
|
-
message: summary
|
|
384
|
+
title: `[Claude ${sessionId}] Task Complete: ${taskName}`,
|
|
385
|
+
message: summary,
|
|
386
|
+
prompt: {
|
|
387
|
+
type: "options",
|
|
388
|
+
options: [
|
|
389
|
+
{ key: "1", label: "Start next task", action: "claude-sm" },
|
|
390
|
+
{ key: "2", label: "View details", action: "stackmemory status" }
|
|
391
|
+
]
|
|
392
|
+
}
|
|
371
393
|
});
|
|
372
394
|
}
|
|
373
395
|
async function notifyError(error, context) {
|
|
396
|
+
const sessionId = getSessionId();
|
|
374
397
|
return sendSMSNotification({
|
|
375
398
|
type: "error",
|
|
376
|
-
title:
|
|
399
|
+
title: `[Claude ${sessionId}] Error Alert`,
|
|
377
400
|
message: context ? `${error}
|
|
378
401
|
|
|
379
|
-
Context: ${context}` : error
|
|
402
|
+
Context: ${context}` : error,
|
|
403
|
+
prompt: {
|
|
404
|
+
type: "options",
|
|
405
|
+
options: [
|
|
406
|
+
{ key: "1", label: "Retry", action: "claude-sm" },
|
|
407
|
+
{
|
|
408
|
+
key: "2",
|
|
409
|
+
label: "View logs",
|
|
410
|
+
action: "tail -50 ~/.claude/logs/*.log"
|
|
411
|
+
}
|
|
412
|
+
]
|
|
413
|
+
}
|
|
380
414
|
});
|
|
381
415
|
}
|
|
382
416
|
function cleanupExpiredPrompts() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/sms-notify.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * SMS Notification Hook for StackMemory\n * Sends text messages when tasks are ready for review\n * Supports interactive prompts with numbered options or yes/no\n *\n * Optional feature - requires Twilio setup\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { config as loadDotenv } from 'dotenv';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport { SMSConfigSchema, parseConfigSafe } from './schemas.js';\n\nexport type MessageChannel = 'whatsapp' | 'sms';\n\nexport interface SMSConfig {\n enabled: boolean;\n // Preferred channel: whatsapp is cheaper for back-and-forth conversations\n channel: MessageChannel;\n // Twilio credentials (from env or config)\n accountSid?: string;\n authToken?: string;\n // SMS numbers\n smsFromNumber?: string;\n smsToNumber?: string;\n // WhatsApp numbers (Twilio prefixes with 'whatsapp:' automatically)\n whatsappFromNumber?: string;\n whatsappToNumber?: string;\n // Legacy fields (backwards compatibility)\n fromNumber?: string;\n toNumber?: string;\n // Webhook URL for receiving responses\n webhookUrl?: string;\n // Notification preferences\n notifyOn: {\n taskComplete: boolean;\n reviewReady: boolean;\n error: boolean;\n custom: boolean;\n };\n // Quiet hours (don't send during these times)\n quietHours?: {\n enabled: boolean;\n start: string; // \"22:00\"\n end: string; // \"08:00\"\n };\n // Response timeout (seconds)\n responseTimeout: number;\n // Pending prompts awaiting response\n pendingPrompts: PendingPrompt[];\n}\n\nexport interface PendingPrompt {\n id: string;\n timestamp: string;\n message: string;\n options: PromptOption[];\n type: 'options' | 'yesno' | 'freeform';\n callback?: string; // Command to run with response\n expiresAt: string;\n}\n\nexport interface PromptOption {\n key: string; // \"1\", \"2\", \"y\", \"n\", etc.\n label: string;\n action?: string; // Command to execute\n}\n\nexport interface NotificationPayload {\n type: 'task_complete' | 'review_ready' | 'error' | 'custom';\n title: string;\n message: string;\n prompt?: {\n type: 'options' | 'yesno' | 'freeform';\n options?: PromptOption[];\n question?: string;\n };\n metadata?: Record<string, unknown>;\n}\n\nconst CONFIG_PATH = join(homedir(), '.stackmemory', 'sms-notify.json');\n\nconst DEFAULT_CONFIG: SMSConfig = {\n enabled: false,\n channel: 'whatsapp', // WhatsApp is cheaper for conversations\n notifyOn: {\n taskComplete: true,\n reviewReady: true,\n error: true,\n custom: true,\n },\n quietHours: {\n enabled: false,\n start: '22:00',\n end: '08:00',\n },\n responseTimeout: 300, // 5 minutes\n pendingPrompts: [],\n};\n\nexport function loadSMSConfig(): SMSConfig {\n // Load .env files (project, home, global)\n loadDotenv({ path: join(process.cwd(), '.env') });\n loadDotenv({ path: join(process.cwd(), '.env.local') });\n loadDotenv({ path: join(homedir(), '.env') });\n loadDotenv({ path: join(homedir(), '.stackmemory', '.env') });\n\n try {\n if (existsSync(CONFIG_PATH)) {\n const data = readFileSync(CONFIG_PATH, 'utf8');\n const parsed = JSON.parse(data);\n // Validate with zod schema, fall back to defaults on invalid config\n const validated = parseConfigSafe(\n SMSConfigSchema,\n { ...DEFAULT_CONFIG, ...parsed },\n DEFAULT_CONFIG,\n 'sms-notify'\n );\n applyEnvVars(validated);\n return validated;\n }\n } catch {\n // Use defaults\n }\n\n // Check environment variables\n const config = { ...DEFAULT_CONFIG };\n applyEnvVars(config);\n return config;\n}\n\n// Check what's missing for notifications to work\nexport function getMissingConfig(): {\n missing: string[];\n configured: string[];\n ready: boolean;\n} {\n const config = loadSMSConfig();\n const missing: string[] = [];\n const configured: string[] = [];\n\n // Check credentials\n if (config.accountSid) {\n configured.push('TWILIO_ACCOUNT_SID');\n } else {\n missing.push('TWILIO_ACCOUNT_SID');\n }\n\n if (config.authToken) {\n configured.push('TWILIO_AUTH_TOKEN');\n } else {\n missing.push('TWILIO_AUTH_TOKEN');\n }\n\n // Check channel-specific numbers\n const channel = config.channel || 'whatsapp';\n\n if (channel === 'whatsapp') {\n const from = config.whatsappFromNumber || config.fromNumber;\n const to = config.whatsappToNumber || config.toNumber;\n\n if (from) {\n configured.push('TWILIO_WHATSAPP_FROM');\n } else {\n missing.push('TWILIO_WHATSAPP_FROM');\n }\n\n if (to) {\n configured.push('TWILIO_WHATSAPP_TO');\n } else {\n missing.push('TWILIO_WHATSAPP_TO');\n }\n } else {\n const from = config.smsFromNumber || config.fromNumber;\n const to = config.smsToNumber || config.toNumber;\n\n if (from) {\n configured.push('TWILIO_SMS_FROM');\n } else {\n missing.push('TWILIO_SMS_FROM');\n }\n\n if (to) {\n configured.push('TWILIO_SMS_TO');\n } else {\n missing.push('TWILIO_SMS_TO');\n }\n }\n\n return {\n missing,\n configured,\n ready: missing.length === 0,\n };\n}\n\nfunction applyEnvVars(config: SMSConfig): void {\n // Twilio credentials\n if (process.env['TWILIO_ACCOUNT_SID']) {\n config.accountSid = process.env['TWILIO_ACCOUNT_SID'];\n }\n if (process.env['TWILIO_AUTH_TOKEN']) {\n config.authToken = process.env['TWILIO_AUTH_TOKEN'];\n }\n\n // SMS numbers\n if (process.env['TWILIO_SMS_FROM'] || process.env['TWILIO_FROM_NUMBER']) {\n config.smsFromNumber =\n process.env['TWILIO_SMS_FROM'] || process.env['TWILIO_FROM_NUMBER'];\n }\n if (process.env['TWILIO_SMS_TO'] || process.env['TWILIO_TO_NUMBER']) {\n config.smsToNumber =\n process.env['TWILIO_SMS_TO'] || process.env['TWILIO_TO_NUMBER'];\n }\n\n // WhatsApp numbers\n if (process.env['TWILIO_WHATSAPP_FROM']) {\n config.whatsappFromNumber = process.env['TWILIO_WHATSAPP_FROM'];\n }\n if (process.env['TWILIO_WHATSAPP_TO']) {\n config.whatsappToNumber = process.env['TWILIO_WHATSAPP_TO'];\n }\n\n // Legacy support\n if (process.env['TWILIO_FROM_NUMBER']) {\n config.fromNumber = process.env['TWILIO_FROM_NUMBER'];\n }\n if (process.env['TWILIO_TO_NUMBER']) {\n config.toNumber = process.env['TWILIO_TO_NUMBER'];\n }\n\n // Channel preference\n if (process.env['TWILIO_CHANNEL']) {\n config.channel = process.env['TWILIO_CHANNEL'] as MessageChannel;\n }\n}\n\nexport function saveSMSConfig(config: SMSConfig): void {\n try {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n // Don't save sensitive credentials to file\n const safeConfig = { ...config };\n delete safeConfig.accountSid;\n delete safeConfig.authToken;\n writeFileSecure(CONFIG_PATH, JSON.stringify(safeConfig, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nfunction isQuietHours(config: SMSConfig): boolean {\n if (!config.quietHours?.enabled) return false;\n\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n\n const [startH, startM] = config.quietHours.start.split(':').map(Number);\n const [endH, endM] = config.quietHours.end.split(':').map(Number);\n\n const startTime = startH * 60 + startM;\n const endTime = endH * 60 + endM;\n\n // Handle overnight quiet hours (e.g., 22:00 - 08:00)\n if (startTime > endTime) {\n return currentTime >= startTime || currentTime < endTime;\n }\n\n return currentTime >= startTime && currentTime < endTime;\n}\n\nfunction generatePromptId(): string {\n return Math.random().toString(36).substring(2, 10);\n}\n\nfunction formatPromptMessage(payload: NotificationPayload): string {\n let message = `${payload.title}\\n\\n${payload.message}`;\n\n if (payload.prompt) {\n message += '\\n\\n';\n\n if (payload.prompt.question) {\n message += `${payload.prompt.question}\\n`;\n }\n\n if (payload.prompt.type === 'yesno') {\n message += 'Reply Y for Yes, N for No';\n } else if (payload.prompt.type === 'options' && payload.prompt.options) {\n payload.prompt.options.forEach((opt) => {\n message += `${opt.key}. ${opt.label}\\n`;\n });\n message += '\\nReply with number to select';\n } else if (payload.prompt.type === 'freeform') {\n message += 'Reply with your response';\n }\n }\n\n return message;\n}\n\nfunction getChannelNumbers(config: SMSConfig): {\n from: string;\n to: string;\n channel: MessageChannel;\n} | null {\n const channel = config.channel || 'whatsapp';\n\n if (channel === 'whatsapp') {\n // Try WhatsApp first\n const from = config.whatsappFromNumber || config.fromNumber;\n const to = config.whatsappToNumber || config.toNumber;\n if (from && to) {\n // Twilio requires 'whatsapp:' prefix for WhatsApp numbers\n return {\n from: from.startsWith('whatsapp:') ? from : `whatsapp:${from}`,\n to: to.startsWith('whatsapp:') ? to : `whatsapp:${to}`,\n channel: 'whatsapp',\n };\n }\n }\n\n // Fall back to SMS\n const from = config.smsFromNumber || config.fromNumber;\n const to = config.smsToNumber || config.toNumber;\n if (from && to) {\n return { from, to, channel: 'sms' };\n }\n\n return null;\n}\n\nexport async function sendNotification(\n payload: NotificationPayload,\n channelOverride?: MessageChannel\n): Promise<{\n success: boolean;\n promptId?: string;\n channel?: MessageChannel;\n error?: string;\n}> {\n const config = loadSMSConfig();\n\n if (!config.enabled) {\n return { success: false, error: 'Notifications disabled' };\n }\n\n // Check notification type is enabled\n const typeMap: Record<string, keyof typeof config.notifyOn> = {\n task_complete: 'taskComplete',\n review_ready: 'reviewReady',\n error: 'error',\n custom: 'custom',\n };\n\n if (!config.notifyOn[typeMap[payload.type]]) {\n return {\n success: false,\n error: `Notifications for ${payload.type} disabled`,\n };\n }\n\n // Check quiet hours\n if (isQuietHours(config)) {\n return { success: false, error: 'Quiet hours active' };\n }\n\n // Validate credentials\n if (!config.accountSid || !config.authToken) {\n return {\n success: false,\n error:\n 'Missing Twilio credentials. Set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN',\n };\n }\n\n // Get channel numbers (prefer WhatsApp)\n const originalChannel = config.channel;\n if (channelOverride) {\n config.channel = channelOverride;\n }\n\n const numbers = getChannelNumbers(config);\n config.channel = originalChannel; // Restore\n\n if (!numbers) {\n return {\n success: false,\n error:\n config.channel === 'whatsapp'\n ? 'Missing WhatsApp numbers. Set TWILIO_WHATSAPP_FROM and TWILIO_WHATSAPP_TO'\n : 'Missing SMS numbers. Set TWILIO_SMS_FROM and TWILIO_SMS_TO',\n };\n }\n\n const message = formatPromptMessage(payload);\n let promptId: string | undefined;\n\n // Store pending prompt if interactive\n if (payload.prompt) {\n promptId = generatePromptId();\n const expiresAt = new Date(\n Date.now() + config.responseTimeout * 1000\n ).toISOString();\n\n const pendingPrompt: PendingPrompt = {\n id: promptId,\n timestamp: new Date().toISOString(),\n message: payload.message,\n options: payload.prompt.options || [],\n type: payload.prompt.type,\n expiresAt,\n };\n\n config.pendingPrompts.push(pendingPrompt);\n saveSMSConfig(config);\n }\n\n try {\n // Use Twilio API (same endpoint for SMS and WhatsApp)\n const twilioUrl = `https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`;\n\n const response = await fetch(twilioUrl, {\n method: 'POST',\n headers: {\n Authorization:\n 'Basic ' +\n Buffer.from(`${config.accountSid}:${config.authToken}`).toString(\n 'base64'\n ),\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n From: numbers.from,\n To: numbers.to,\n Body: message,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.text();\n return {\n success: false,\n channel: numbers.channel,\n error: `Twilio error: ${errorData}`,\n };\n }\n\n return { success: true, promptId, channel: numbers.channel };\n } catch (err) {\n return {\n success: false,\n channel: numbers.channel,\n error: `Failed to send ${numbers.channel}: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n}\n\n// Backwards compatible alias\nexport async function sendSMSNotification(\n payload: NotificationPayload\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n return sendNotification(payload);\n}\n\nexport function processIncomingResponse(\n from: string,\n body: string\n): {\n matched: boolean;\n prompt?: PendingPrompt;\n response?: string;\n action?: string;\n} {\n const config = loadSMSConfig();\n\n // Normalize response\n const response = body.trim().toLowerCase();\n\n // Find matching pending prompt (most recent first)\n const now = new Date();\n const validPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n if (validPrompts.length === 0) {\n return { matched: false };\n }\n\n // Get most recent prompt\n const prompt = validPrompts[validPrompts.length - 1];\n\n let matchedOption: PromptOption | undefined;\n\n if (prompt.type === 'yesno') {\n if (response === 'y' || response === 'yes') {\n matchedOption = { key: 'y', label: 'Yes' };\n } else if (response === 'n' || response === 'no') {\n matchedOption = { key: 'n', label: 'No' };\n }\n } else if (prompt.type === 'options') {\n matchedOption = prompt.options.find(\n (opt) => opt.key.toLowerCase() === response\n );\n } else if (prompt.type === 'freeform') {\n matchedOption = { key: response, label: response };\n }\n\n // Remove processed prompt\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => p.id !== prompt.id\n );\n saveSMSConfig(config);\n\n if (matchedOption) {\n return {\n matched: true,\n prompt,\n response: matchedOption.key,\n action: matchedOption.action,\n };\n }\n\n return { matched: false, prompt };\n}\n\n// Convenience functions for common notifications\n\nexport async function notifyReviewReady(\n title: string,\n description: string,\n options?: { label: string; action?: string }[]\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const payload: NotificationPayload = {\n type: 'review_ready',\n title: `Review Ready: ${title}`,\n message: description,\n };\n\n if (options && options.length > 0) {\n payload.prompt = {\n type: 'options',\n options: options.map((opt, i) => ({\n key: String(i + 1),\n label: opt.label,\n action: opt.action,\n })),\n question: 'What would you like to do?',\n };\n }\n\n return sendSMSNotification(payload);\n}\n\nexport async function notifyWithYesNo(\n title: string,\n question: string,\n yesAction?: string,\n noAction?: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n return sendSMSNotification({\n type: 'custom',\n title,\n message: question,\n prompt: {\n type: 'yesno',\n options: [\n { key: 'y', label: 'Yes', action: yesAction },\n { key: 'n', label: 'No', action: noAction },\n ],\n },\n });\n}\n\nexport async function notifyTaskComplete(\n taskName: string,\n summary: string\n): Promise<{ success: boolean; error?: string }> {\n return sendSMSNotification({\n type: 'task_complete',\n title: `Task Complete: ${taskName}`,\n message: summary,\n });\n}\n\nexport async function notifyError(\n error: string,\n context?: string\n): Promise<{ success: boolean; error?: string }> {\n return sendSMSNotification({\n type: 'error',\n title: 'Error Alert',\n message: context ? `${error}\\n\\nContext: ${context}` : error,\n });\n}\n\n// Clean up expired prompts\nexport function cleanupExpiredPrompts(): number {\n const config = loadSMSConfig();\n const now = new Date();\n const before = config.pendingPrompts.length;\n\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n const removed = before - config.pendingPrompts.length;\n if (removed > 0) {\n saveSMSConfig(config);\n }\n\n return removed;\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * SMS Notification Hook for StackMemory\n * Sends text messages when tasks are ready for review\n * Supports interactive prompts with numbered options or yes/no\n *\n * Optional feature - requires Twilio setup\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { config as loadDotenv } from 'dotenv';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport { SMSConfigSchema, parseConfigSafe } from './schemas.js';\n\nexport type MessageChannel = 'whatsapp' | 'sms';\n\nexport interface SMSConfig {\n enabled: boolean;\n // Preferred channel: whatsapp is cheaper for back-and-forth conversations\n channel: MessageChannel;\n // Twilio credentials (from env or config)\n accountSid?: string;\n authToken?: string;\n // SMS numbers\n smsFromNumber?: string;\n smsToNumber?: string;\n // WhatsApp numbers (Twilio prefixes with 'whatsapp:' automatically)\n whatsappFromNumber?: string;\n whatsappToNumber?: string;\n // Legacy fields (backwards compatibility)\n fromNumber?: string;\n toNumber?: string;\n // Webhook URL for receiving responses\n webhookUrl?: string;\n // Notification preferences\n notifyOn: {\n taskComplete: boolean;\n reviewReady: boolean;\n error: boolean;\n custom: boolean;\n };\n // Quiet hours (don't send during these times)\n quietHours?: {\n enabled: boolean;\n start: string; // \"22:00\"\n end: string; // \"08:00\"\n };\n // Response timeout (seconds)\n responseTimeout: number;\n // Pending prompts awaiting response\n pendingPrompts: PendingPrompt[];\n}\n\nexport interface PendingPrompt {\n id: string;\n timestamp: string;\n message: string;\n options: PromptOption[];\n type: 'options' | 'yesno' | 'freeform';\n callback?: string; // Command to run with response\n expiresAt: string;\n}\n\nexport interface PromptOption {\n key: string; // \"1\", \"2\", \"y\", \"n\", etc.\n label: string;\n action?: string; // Command to execute\n}\n\nexport interface NotificationPayload {\n type: 'task_complete' | 'review_ready' | 'error' | 'custom';\n title: string;\n message: string;\n prompt?: {\n type: 'options' | 'yesno' | 'freeform';\n options?: PromptOption[];\n question?: string;\n };\n metadata?: Record<string, unknown>;\n}\n\nconst CONFIG_PATH = join(homedir(), '.stackmemory', 'sms-notify.json');\n\nconst DEFAULT_CONFIG: SMSConfig = {\n enabled: false,\n channel: 'whatsapp', // WhatsApp is cheaper for conversations\n notifyOn: {\n taskComplete: true,\n reviewReady: true,\n error: true,\n custom: true,\n },\n quietHours: {\n enabled: false,\n start: '22:00',\n end: '08:00',\n },\n responseTimeout: 300, // 5 minutes\n pendingPrompts: [],\n};\n\nexport function loadSMSConfig(): SMSConfig {\n // Load .env files (project, home, global)\n loadDotenv({ path: join(process.cwd(), '.env') });\n loadDotenv({ path: join(process.cwd(), '.env.local') });\n loadDotenv({ path: join(homedir(), '.env') });\n loadDotenv({ path: join(homedir(), '.stackmemory', '.env') });\n\n try {\n if (existsSync(CONFIG_PATH)) {\n const data = readFileSync(CONFIG_PATH, 'utf8');\n const parsed = JSON.parse(data);\n // Validate with zod schema, fall back to defaults on invalid config\n const validated = parseConfigSafe(\n SMSConfigSchema,\n { ...DEFAULT_CONFIG, ...parsed },\n DEFAULT_CONFIG,\n 'sms-notify'\n );\n applyEnvVars(validated);\n return validated;\n }\n } catch {\n // Use defaults\n }\n\n // Check environment variables\n const config = { ...DEFAULT_CONFIG };\n applyEnvVars(config);\n return config;\n}\n\n// Check what's missing for notifications to work\nexport function getMissingConfig(): {\n missing: string[];\n configured: string[];\n ready: boolean;\n} {\n const config = loadSMSConfig();\n const missing: string[] = [];\n const configured: string[] = [];\n\n // Check credentials\n if (config.accountSid) {\n configured.push('TWILIO_ACCOUNT_SID');\n } else {\n missing.push('TWILIO_ACCOUNT_SID');\n }\n\n if (config.authToken) {\n configured.push('TWILIO_AUTH_TOKEN');\n } else {\n missing.push('TWILIO_AUTH_TOKEN');\n }\n\n // Check channel-specific numbers\n const channel = config.channel || 'whatsapp';\n\n if (channel === 'whatsapp') {\n const from = config.whatsappFromNumber || config.fromNumber;\n const to = config.whatsappToNumber || config.toNumber;\n\n if (from) {\n configured.push('TWILIO_WHATSAPP_FROM');\n } else {\n missing.push('TWILIO_WHATSAPP_FROM');\n }\n\n if (to) {\n configured.push('TWILIO_WHATSAPP_TO');\n } else {\n missing.push('TWILIO_WHATSAPP_TO');\n }\n } else {\n const from = config.smsFromNumber || config.fromNumber;\n const to = config.smsToNumber || config.toNumber;\n\n if (from) {\n configured.push('TWILIO_SMS_FROM');\n } else {\n missing.push('TWILIO_SMS_FROM');\n }\n\n if (to) {\n configured.push('TWILIO_SMS_TO');\n } else {\n missing.push('TWILIO_SMS_TO');\n }\n }\n\n return {\n missing,\n configured,\n ready: missing.length === 0,\n };\n}\n\nfunction applyEnvVars(config: SMSConfig): void {\n // Twilio credentials\n if (process.env['TWILIO_ACCOUNT_SID']) {\n config.accountSid = process.env['TWILIO_ACCOUNT_SID'];\n }\n if (process.env['TWILIO_AUTH_TOKEN']) {\n config.authToken = process.env['TWILIO_AUTH_TOKEN'];\n }\n\n // SMS numbers\n if (process.env['TWILIO_SMS_FROM'] || process.env['TWILIO_FROM_NUMBER']) {\n config.smsFromNumber =\n process.env['TWILIO_SMS_FROM'] || process.env['TWILIO_FROM_NUMBER'];\n }\n if (process.env['TWILIO_SMS_TO'] || process.env['TWILIO_TO_NUMBER']) {\n config.smsToNumber =\n process.env['TWILIO_SMS_TO'] || process.env['TWILIO_TO_NUMBER'];\n }\n\n // WhatsApp numbers\n if (process.env['TWILIO_WHATSAPP_FROM']) {\n config.whatsappFromNumber = process.env['TWILIO_WHATSAPP_FROM'];\n }\n if (process.env['TWILIO_WHATSAPP_TO']) {\n config.whatsappToNumber = process.env['TWILIO_WHATSAPP_TO'];\n }\n\n // Legacy support\n if (process.env['TWILIO_FROM_NUMBER']) {\n config.fromNumber = process.env['TWILIO_FROM_NUMBER'];\n }\n if (process.env['TWILIO_TO_NUMBER']) {\n config.toNumber = process.env['TWILIO_TO_NUMBER'];\n }\n\n // Channel preference\n if (process.env['TWILIO_CHANNEL']) {\n config.channel = process.env['TWILIO_CHANNEL'] as MessageChannel;\n }\n}\n\nexport function saveSMSConfig(config: SMSConfig): void {\n try {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n // Don't save sensitive credentials to file\n const safeConfig = { ...config };\n delete safeConfig.accountSid;\n delete safeConfig.authToken;\n writeFileSecure(CONFIG_PATH, JSON.stringify(safeConfig, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nfunction isQuietHours(config: SMSConfig): boolean {\n if (!config.quietHours?.enabled) return false;\n\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n\n const [startH, startM] = config.quietHours.start.split(':').map(Number);\n const [endH, endM] = config.quietHours.end.split(':').map(Number);\n\n const startTime = startH * 60 + startM;\n const endTime = endH * 60 + endM;\n\n // Handle overnight quiet hours (e.g., 22:00 - 08:00)\n if (startTime > endTime) {\n return currentTime >= startTime || currentTime < endTime;\n }\n\n return currentTime >= startTime && currentTime < endTime;\n}\n\nfunction generatePromptId(): string {\n return Math.random().toString(36).substring(2, 10);\n}\n\nfunction formatPromptMessage(payload: NotificationPayload): string {\n let message = `${payload.title}\\n\\n${payload.message}`;\n\n if (payload.prompt) {\n message += '\\n\\n';\n\n if (payload.prompt.question) {\n message += `${payload.prompt.question}\\n`;\n }\n\n if (payload.prompt.type === 'yesno') {\n message += 'Reply Y for Yes, N for No';\n } else if (payload.prompt.type === 'options' && payload.prompt.options) {\n payload.prompt.options.forEach((opt) => {\n message += `${opt.key}. ${opt.label}\\n`;\n });\n message += '\\nReply with number to select';\n } else if (payload.prompt.type === 'freeform') {\n message += 'Reply with your response';\n }\n }\n\n return message;\n}\n\nfunction getChannelNumbers(config: SMSConfig): {\n from: string;\n to: string;\n channel: MessageChannel;\n} | null {\n const channel = config.channel || 'whatsapp';\n\n if (channel === 'whatsapp') {\n // Try WhatsApp first\n const from = config.whatsappFromNumber || config.fromNumber;\n const to = config.whatsappToNumber || config.toNumber;\n if (from && to) {\n // Twilio requires 'whatsapp:' prefix for WhatsApp numbers\n return {\n from: from.startsWith('whatsapp:') ? from : `whatsapp:${from}`,\n to: to.startsWith('whatsapp:') ? to : `whatsapp:${to}`,\n channel: 'whatsapp',\n };\n }\n }\n\n // Fall back to SMS\n const from = config.smsFromNumber || config.fromNumber;\n const to = config.smsToNumber || config.toNumber;\n if (from && to) {\n return { from, to, channel: 'sms' };\n }\n\n return null;\n}\n\nexport async function sendNotification(\n payload: NotificationPayload,\n channelOverride?: MessageChannel\n): Promise<{\n success: boolean;\n promptId?: string;\n channel?: MessageChannel;\n error?: string;\n}> {\n const config = loadSMSConfig();\n\n if (!config.enabled) {\n return { success: false, error: 'Notifications disabled' };\n }\n\n // Check notification type is enabled\n const typeMap: Record<string, keyof typeof config.notifyOn> = {\n task_complete: 'taskComplete',\n review_ready: 'reviewReady',\n error: 'error',\n custom: 'custom',\n };\n\n if (!config.notifyOn[typeMap[payload.type]]) {\n return {\n success: false,\n error: `Notifications for ${payload.type} disabled`,\n };\n }\n\n // Check quiet hours\n if (isQuietHours(config)) {\n return { success: false, error: 'Quiet hours active' };\n }\n\n // Validate credentials\n if (!config.accountSid || !config.authToken) {\n return {\n success: false,\n error:\n 'Missing Twilio credentials. Set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN',\n };\n }\n\n // Get channel numbers (prefer WhatsApp)\n const originalChannel = config.channel;\n if (channelOverride) {\n config.channel = channelOverride;\n }\n\n const numbers = getChannelNumbers(config);\n config.channel = originalChannel; // Restore\n\n if (!numbers) {\n return {\n success: false,\n error:\n config.channel === 'whatsapp'\n ? 'Missing WhatsApp numbers. Set TWILIO_WHATSAPP_FROM and TWILIO_WHATSAPP_TO'\n : 'Missing SMS numbers. Set TWILIO_SMS_FROM and TWILIO_SMS_TO',\n };\n }\n\n const message = formatPromptMessage(payload);\n let promptId: string | undefined;\n\n // Store pending prompt if interactive\n if (payload.prompt) {\n promptId = generatePromptId();\n const expiresAt = new Date(\n Date.now() + config.responseTimeout * 1000\n ).toISOString();\n\n const pendingPrompt: PendingPrompt = {\n id: promptId,\n timestamp: new Date().toISOString(),\n message: payload.message,\n options: payload.prompt.options || [],\n type: payload.prompt.type,\n expiresAt,\n };\n\n config.pendingPrompts.push(pendingPrompt);\n saveSMSConfig(config);\n }\n\n try {\n // Use Twilio API (same endpoint for SMS and WhatsApp)\n const twilioUrl = `https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`;\n\n const response = await fetch(twilioUrl, {\n method: 'POST',\n headers: {\n Authorization:\n 'Basic ' +\n Buffer.from(`${config.accountSid}:${config.authToken}`).toString(\n 'base64'\n ),\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n From: numbers.from,\n To: numbers.to,\n Body: message,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.text();\n return {\n success: false,\n channel: numbers.channel,\n error: `Twilio error: ${errorData}`,\n };\n }\n\n return { success: true, promptId, channel: numbers.channel };\n } catch (err) {\n return {\n success: false,\n channel: numbers.channel,\n error: `Failed to send ${numbers.channel}: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n}\n\n// Backwards compatible alias\nexport async function sendSMSNotification(\n payload: NotificationPayload\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n return sendNotification(payload);\n}\n\nexport function processIncomingResponse(\n from: string,\n body: string\n): {\n matched: boolean;\n prompt?: PendingPrompt;\n response?: string;\n action?: string;\n} {\n const config = loadSMSConfig();\n\n // Normalize response\n const response = body.trim().toLowerCase();\n\n // Find matching pending prompt (most recent first)\n const now = new Date();\n const validPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n if (validPrompts.length === 0) {\n return { matched: false };\n }\n\n // Get most recent prompt\n const prompt = validPrompts[validPrompts.length - 1];\n\n let matchedOption: PromptOption | undefined;\n\n if (prompt.type === 'yesno') {\n if (response === 'y' || response === 'yes') {\n matchedOption = { key: 'y', label: 'Yes' };\n } else if (response === 'n' || response === 'no') {\n matchedOption = { key: 'n', label: 'No' };\n }\n } else if (prompt.type === 'options') {\n matchedOption = prompt.options.find(\n (opt) => opt.key.toLowerCase() === response\n );\n } else if (prompt.type === 'freeform') {\n matchedOption = { key: response, label: response };\n }\n\n // Remove processed prompt\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => p.id !== prompt.id\n );\n saveSMSConfig(config);\n\n if (matchedOption) {\n return {\n matched: true,\n prompt,\n response: matchedOption.key,\n action: matchedOption.action,\n };\n }\n\n return { matched: false, prompt };\n}\n\n// Get session ID from environment or generate short ID\nfunction getSessionId(): string {\n return (\n process.env['CLAUDE_INSTANCE_ID'] ||\n process.env['STACKMEMORY_SESSION_ID'] ||\n Math.random().toString(36).substring(2, 8)\n );\n}\n\n// Convenience functions for common notifications\n\nexport async function notifyReviewReady(\n title: string,\n description: string,\n options?: { label: string; action?: string }[]\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n\n // Ensure minimum 2 options\n let finalOptions = options || [];\n if (finalOptions.length < 2) {\n const defaults = [\n { label: 'Approve', action: 'echo \"Approved\"' },\n { label: 'Request changes', action: 'echo \"Changes requested\"' },\n ];\n finalOptions = [...finalOptions, ...defaults].slice(\n 0,\n Math.max(2, finalOptions.length)\n );\n }\n\n const payload: NotificationPayload = {\n type: 'review_ready',\n title: `[Claude ${sessionId}] Review Ready: ${title}`,\n message: description,\n prompt: {\n type: 'options',\n options: finalOptions.map((opt, i) => ({\n key: String(i + 1),\n label: opt.label,\n action: opt.action,\n })),\n question: 'What would you like to do?',\n },\n };\n\n return sendSMSNotification(payload);\n}\n\nexport async function notifyWithYesNo(\n title: string,\n question: string,\n yesAction?: string,\n noAction?: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n return sendSMSNotification({\n type: 'custom',\n title: `[Claude ${sessionId}] ${title}`,\n message: question,\n prompt: {\n type: 'yesno',\n options: [\n { key: 'y', label: 'Yes', action: yesAction },\n { key: 'n', label: 'No', action: noAction },\n ],\n },\n });\n}\n\nexport async function notifyTaskComplete(\n taskName: string,\n summary: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n return sendSMSNotification({\n type: 'task_complete',\n title: `[Claude ${sessionId}] Task Complete: ${taskName}`,\n message: summary,\n prompt: {\n type: 'options',\n options: [\n { key: '1', label: 'Start next task', action: 'claude-sm' },\n { key: '2', label: 'View details', action: 'stackmemory status' },\n ],\n },\n });\n}\n\nexport async function notifyError(\n error: string,\n context?: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n return sendSMSNotification({\n type: 'error',\n title: `[Claude ${sessionId}] Error Alert`,\n message: context ? `${error}\\n\\nContext: ${context}` : error,\n prompt: {\n type: 'options',\n options: [\n { key: '1', label: 'Retry', action: 'claude-sm' },\n {\n key: '2',\n label: 'View logs',\n action: 'tail -50 ~/.claude/logs/*.log',\n },\n ],\n },\n });\n}\n\n// Clean up expired prompts\nexport function cleanupExpiredPrompts(): number {\n const config = loadSMSConfig();\n const now = new Date();\n const before = config.pendingPrompts.length;\n\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n const removed = before - config.pendingPrompts.length;\n if (removed > 0) {\n saveSMSConfig(config);\n }\n\n return removed;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["from", "to"]
|
|
7
7
|
}
|
|
@@ -6,17 +6,7 @@ import { createHash, randomBytes } from "crypto";
|
|
|
6
6
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
import { logger } from "../../core/monitoring/logger.js";
|
|
9
|
-
|
|
10
|
-
const value = process.env[key];
|
|
11
|
-
if (value === void 0) {
|
|
12
|
-
if (defaultValue !== void 0) return defaultValue;
|
|
13
|
-
throw new Error(`Environment variable ${key} is required`);
|
|
14
|
-
}
|
|
15
|
-
return value;
|
|
16
|
-
}
|
|
17
|
-
function getOptionalEnv(key) {
|
|
18
|
-
return process.env[key];
|
|
19
|
-
}
|
|
9
|
+
import { IntegrationError, ErrorCode } from "../../core/errors/index.js";
|
|
20
10
|
class LinearAuthManager {
|
|
21
11
|
configPath;
|
|
22
12
|
tokensPath;
|
|
@@ -61,7 +51,10 @@ class LinearAuthManager {
|
|
|
61
51
|
*/
|
|
62
52
|
generateAuthUrl(state) {
|
|
63
53
|
if (!this.config) {
|
|
64
|
-
throw new
|
|
54
|
+
throw new IntegrationError(
|
|
55
|
+
"Linear OAuth configuration not loaded",
|
|
56
|
+
ErrorCode.LINEAR_AUTH_FAILED
|
|
57
|
+
);
|
|
65
58
|
}
|
|
66
59
|
const codeVerifier = randomBytes(32).toString("base64url");
|
|
67
60
|
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
|
|
@@ -86,7 +79,10 @@ class LinearAuthManager {
|
|
|
86
79
|
*/
|
|
87
80
|
async exchangeCodeForToken(authCode, codeVerifier) {
|
|
88
81
|
if (!this.config) {
|
|
89
|
-
throw new
|
|
82
|
+
throw new IntegrationError(
|
|
83
|
+
"Linear OAuth configuration not loaded",
|
|
84
|
+
ErrorCode.LINEAR_AUTH_FAILED
|
|
85
|
+
);
|
|
90
86
|
}
|
|
91
87
|
const tokenUrl = "https://api.linear.app/oauth/token";
|
|
92
88
|
const body = new URLSearchParams({
|
|
@@ -107,7 +103,11 @@ class LinearAuthManager {
|
|
|
107
103
|
});
|
|
108
104
|
if (!response.ok) {
|
|
109
105
|
const errorText = await response.text();
|
|
110
|
-
throw new
|
|
106
|
+
throw new IntegrationError(
|
|
107
|
+
`Token exchange failed: ${response.status}`,
|
|
108
|
+
ErrorCode.LINEAR_AUTH_FAILED,
|
|
109
|
+
{ status: response.status, body: errorText }
|
|
110
|
+
);
|
|
111
111
|
}
|
|
112
112
|
const result = await response.json();
|
|
113
113
|
const expiresAt = Date.now() + result.expiresIn * 1e3;
|
|
@@ -125,11 +125,17 @@ class LinearAuthManager {
|
|
|
125
125
|
*/
|
|
126
126
|
async refreshAccessToken() {
|
|
127
127
|
if (!this.config) {
|
|
128
|
-
throw new
|
|
128
|
+
throw new IntegrationError(
|
|
129
|
+
"Linear OAuth configuration not loaded",
|
|
130
|
+
ErrorCode.LINEAR_AUTH_FAILED
|
|
131
|
+
);
|
|
129
132
|
}
|
|
130
133
|
const currentTokens = this.loadTokens();
|
|
131
134
|
if (!currentTokens?.refreshToken) {
|
|
132
|
-
throw new
|
|
135
|
+
throw new IntegrationError(
|
|
136
|
+
"No refresh token available",
|
|
137
|
+
ErrorCode.LINEAR_AUTH_FAILED
|
|
138
|
+
);
|
|
133
139
|
}
|
|
134
140
|
const tokenUrl = "https://api.linear.app/oauth/token";
|
|
135
141
|
const body = new URLSearchParams({
|
|
@@ -148,7 +154,11 @@ class LinearAuthManager {
|
|
|
148
154
|
});
|
|
149
155
|
if (!response.ok) {
|
|
150
156
|
const errorText = await response.text();
|
|
151
|
-
throw new
|
|
157
|
+
throw new IntegrationError(
|
|
158
|
+
`Token refresh failed: ${response.status}`,
|
|
159
|
+
ErrorCode.LINEAR_AUTH_FAILED,
|
|
160
|
+
{ status: response.status, body: errorText }
|
|
161
|
+
);
|
|
152
162
|
}
|
|
153
163
|
const result = await response.json();
|
|
154
164
|
const tokens = {
|
|
@@ -166,7 +176,10 @@ class LinearAuthManager {
|
|
|
166
176
|
async getValidToken() {
|
|
167
177
|
const tokens = this.loadTokens();
|
|
168
178
|
if (!tokens) {
|
|
169
|
-
throw new
|
|
179
|
+
throw new IntegrationError(
|
|
180
|
+
"No Linear tokens found. Please complete OAuth setup.",
|
|
181
|
+
ErrorCode.LINEAR_AUTH_FAILED
|
|
182
|
+
);
|
|
170
183
|
}
|
|
171
184
|
const fiveMinutes = 5 * 60 * 1e3;
|
|
172
185
|
if (tokens.expiresAt - Date.now() < fiveMinutes) {
|
|
@@ -270,8 +283,9 @@ class LinearOAuthSetup {
|
|
|
270
283
|
try {
|
|
271
284
|
const codeVerifier = process.env["_LINEAR_CODE_VERIFIER"];
|
|
272
285
|
if (!codeVerifier) {
|
|
273
|
-
throw new
|
|
274
|
-
"Code verifier not found. Please restart the setup process."
|
|
286
|
+
throw new IntegrationError(
|
|
287
|
+
"Code verifier not found. Please restart the setup process.",
|
|
288
|
+
ErrorCode.LINEAR_AUTH_FAILED
|
|
275
289
|
);
|
|
276
290
|
}
|
|
277
291
|
await this.authManager.exchangeCodeForToken(authCode, codeVerifier);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/integrations/linear/auth.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Linear OAuth Authentication Setup\n * Handles initial OAuth flow and token management for Linear integration\n */\n\nimport { createHash, randomBytes } from 'crypto';\nimport { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { join } from 'path';\nimport { logger } from '../../core/monitoring/logger.js';\n// Type-safe environment variable access\nfunction getEnv(key: string, defaultValue?: string): string {\n const value = process.env[key];\n if (value === undefined) {\n if (defaultValue !== undefined) return defaultValue;\n throw new Error(`Environment variable ${key} is required`);\n }\n return value;\n}\n\nfunction getOptionalEnv(key: string): string | undefined {\n return process.env[key];\n}\n\n\nexport interface LinearAuthConfig {\n clientId: string;\n clientSecret: string;\n redirectUri: string;\n scopes: string[];\n}\n\nexport interface LinearTokens {\n accessToken: string;\n refreshToken?: string;\n expiresAt: number; // Unix timestamp\n scope: string[];\n}\n\nexport interface LinearAuthResult {\n accessToken: string;\n refreshToken?: string;\n expiresIn: number;\n scope: string;\n tokenType: string;\n}\n\nexport class LinearAuthManager {\n private configPath: string;\n private tokensPath: string;\n private config?: LinearAuthConfig;\n\n constructor(projectRoot: string) {\n const configDir = join(projectRoot, '.stackmemory');\n this.configPath = join(configDir, 'linear-config.json');\n this.tokensPath = join(configDir, 'linear-tokens.json');\n }\n\n /**\n * Check if Linear integration is configured\n */\n isConfigured(): boolean {\n return existsSync(this.configPath) && existsSync(this.tokensPath);\n }\n\n /**\n * Save OAuth application configuration\n */\n saveConfig(config: LinearAuthConfig): void {\n writeFileSync(this.configPath, JSON.stringify(config, null, 2));\n this.config = config;\n logger.info('Linear OAuth configuration saved');\n }\n\n /**\n * Load OAuth configuration\n */\n loadConfig(): LinearAuthConfig | null {\n if (!existsSync(this.configPath)) {\n return null;\n }\n\n try {\n const configData = readFileSync(this.configPath, 'utf8');\n this.config = JSON.parse(configData);\n return this.config!;\n } catch (error: unknown) {\n logger.error('Failed to load Linear configuration:', error as Error);\n return null;\n }\n }\n\n /**\n * Generate OAuth authorization URL with PKCE\n */\n generateAuthUrl(state?: string): { url: string; codeVerifier: string } {\n if (!this.config) {\n throw new Error('Linear OAuth configuration not loaded');\n }\n\n // Generate PKCE parameters\n const codeVerifier = randomBytes(32).toString('base64url');\n const codeChallenge = createHash('sha256')\n .update(codeVerifier)\n .digest('base64url');\n\n const params = new URLSearchParams({\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n response_type: 'code',\n scope: this.config.scopes.join(' '),\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n actor: 'app', // Enable actor authorization for service accounts\n });\n\n if (state) {\n params.set('state', state);\n }\n\n const authUrl = `https://linear.app/oauth/authorize?${params.toString()}`;\n\n return { url: authUrl, codeVerifier };\n }\n\n /**\n * Exchange authorization code for access token\n */\n async exchangeCodeForToken(\n authCode: string,\n codeVerifier: string\n ): Promise<LinearTokens> {\n if (!this.config) {\n throw new Error('Linear OAuth configuration not loaded');\n }\n\n const tokenUrl = 'https://api.linear.app/oauth/token';\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: this.config.clientId,\n client_secret: this.config.clientSecret,\n redirect_uri: this.config.redirectUri,\n code: authCode,\n code_verifier: codeVerifier,\n });\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token exchange failed: ${response.status} ${errorText}`);\n }\n\n const result = (await response.json()) as LinearAuthResult;\n\n // Calculate expiration time (tokens expire in 24 hours)\n const expiresAt = Date.now() + result.expiresIn * 1000;\n\n const tokens: LinearTokens = {\n accessToken: result.accessToken,\n refreshToken: result.refreshToken,\n expiresAt,\n scope: result.scope.split(' '),\n };\n\n this.saveTokens(tokens);\n return tokens;\n }\n\n /**\n * Refresh access token using refresh token\n */\n async refreshAccessToken(): Promise<LinearTokens> {\n if (!this.config) {\n throw new Error('Linear OAuth configuration not loaded');\n }\n\n const currentTokens = this.loadTokens();\n if (!currentTokens?.refreshToken) {\n throw new Error('No refresh token available');\n }\n\n const tokenUrl = 'https://api.linear.app/oauth/token';\n\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: this.config.clientId,\n client_secret: this.config.clientSecret,\n refresh_token: currentTokens.refreshToken,\n });\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed: ${response.status} ${errorText}`);\n }\n\n const result = (await response.json()) as LinearAuthResult;\n\n const tokens: LinearTokens = {\n accessToken: result.accessToken,\n refreshToken: result.refreshToken || currentTokens.refreshToken,\n expiresAt: Date.now() + result.expiresIn * 1000,\n scope: result.scope.split(' '),\n };\n\n this.saveTokens(tokens);\n return tokens;\n }\n\n /**\n * Get valid access token (refresh if needed)\n */\n async getValidToken(): Promise<string> {\n const tokens = this.loadTokens();\n if (!tokens) {\n throw new Error('No Linear tokens found. Please complete OAuth setup.');\n }\n\n // Check if token expires in next 5 minutes\n const fiveMinutes = 5 * 60 * 1000;\n if (tokens.expiresAt - Date.now() < fiveMinutes) {\n logger.info('Linear token expiring soon, refreshing...');\n const newTokens = await this.refreshAccessToken();\n return newTokens.accessToken;\n }\n\n return tokens.accessToken;\n }\n\n /**\n * Save tokens to file\n */\n private saveTokens(tokens: LinearTokens): void {\n writeFileSync(this.tokensPath, JSON.stringify(tokens, null, 2));\n logger.info('Linear tokens saved');\n }\n\n /**\n * Load tokens from file\n */\n loadTokens(): LinearTokens | null {\n if (!existsSync(this.tokensPath)) {\n return null;\n }\n\n try {\n const tokensData = readFileSync(this.tokensPath, 'utf8');\n return JSON.parse(tokensData);\n } catch (error: unknown) {\n logger.error('Failed to load Linear tokens:', error as Error);\n return null;\n }\n }\n\n /**\n * Clear stored tokens and config\n */\n clearAuth(): void {\n if (existsSync(this.tokensPath)) {\n writeFileSync(this.tokensPath, '');\n }\n if (existsSync(this.configPath)) {\n writeFileSync(this.configPath, '');\n }\n logger.info('Linear authentication cleared');\n }\n}\n\n/**\n * Default Linear OAuth scopes for task management\n */\nexport const DEFAULT_LINEAR_SCOPES = [\n 'read', // Read issues, projects, teams\n 'write', // Create and update issues\n 'admin', // Manage team settings and workflows\n];\n\n/**\n * Linear OAuth setup helper\n */\nexport class LinearOAuthSetup {\n private authManager: LinearAuthManager;\n\n constructor(projectRoot: string) {\n this.authManager = new LinearAuthManager(projectRoot);\n }\n\n /**\n * Interactive setup for Linear OAuth\n */\n async setupInteractive(): Promise<{\n authUrl: string;\n instructions: string[];\n }> {\n // For now, we'll provide manual setup instructions\n // In a full implementation, this could open a browser or use a local server\n\n const config: LinearAuthConfig = {\n clientId: process.env['LINEAR_CLIENT_ID'] || '',\n clientSecret: process.env['LINEAR_CLIENT_SECRET'] || '',\n redirectUri:\n process.env['LINEAR_REDIRECT_URI'] ||\n 'http://localhost:3456/auth/linear/callback',\n scopes: DEFAULT_LINEAR_SCOPES,\n };\n\n if (!config.clientId || !config.clientSecret) {\n return {\n authUrl: '',\n instructions: [\n '1. Create a Linear OAuth application at https://linear.app/settings/api',\n '2. Set redirect URI to: http://localhost:3456/auth/linear/callback',\n '3. Copy your Client ID and Client Secret',\n '4. Set environment variables:',\n ' export LINEAR_CLIENT_ID=\"your_client_id\"',\n ' export LINEAR_CLIENT_SECRET=\"your_client_secret\"',\n '5. Re-run this setup command',\n ],\n };\n }\n\n this.authManager.saveConfig(config);\n\n const { url, codeVerifier } = this.authManager.generateAuthUrl();\n\n // Store code verifier temporarily (in a real app, this would be in a secure session store)\n process.env['_LINEAR_CODE_VERIFIER'] = codeVerifier;\n\n return {\n authUrl: url,\n instructions: [\n '1. Open this URL in your browser:',\n url,\n '',\n '2. Approve the StackMemory integration',\n '3. Copy the authorization code from the redirect URL',\n '4. Run: stackmemory linear authorize <code>',\n ],\n };\n }\n\n /**\n * Complete OAuth flow with authorization code\n */\n async completeAuth(authCode: string): Promise<boolean> {\n try {\n const codeVerifier = process.env['_LINEAR_CODE_VERIFIER'];\n if (!codeVerifier) {\n throw new Error(\n 'Code verifier not found. Please restart the setup process.'\n );\n }\n\n await this.authManager.exchangeCodeForToken(authCode, codeVerifier);\n delete process.env['_LINEAR_CODE_VERIFIER'];\n\n logger.info('Linear OAuth setup completed successfully!');\n return true;\n } catch (error: unknown) {\n logger.error('Failed to complete Linear OAuth setup:', error as Error);\n return false;\n }\n }\n\n /**\n * Test the Linear connection\n */\n async testConnection(): Promise<boolean> {\n try {\n const token = await this.authManager.getValidToken();\n\n // Test with a simple GraphQL query\n const response = await fetch('https://api.linear.app/graphql', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n query: 'query { viewer { id name email } }',\n }),\n });\n\n if (response.ok) {\n const result = (await response.json()) as {\n data?: { viewer?: { id: string; name: string; email: string } };\n };\n if (result.data?.viewer) {\n logger.info(\n `Connected to Linear as: ${result.data.viewer.name} (${result.data.viewer.email})`\n );\n return true;\n }\n }\n\n return false;\n } catch (error: unknown) {\n logger.error('Linear connection test failed:', error as Error);\n return false;\n }\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;AAKA,SAAS,YAAY,mBAAmB;AACxC,SAAS,cAAc,eAAe,kBAAkB;AACxD,SAAS,YAAY;AACrB,SAAS,cAAc;
|
|
4
|
+
"sourcesContent": ["/**\n * Linear OAuth Authentication Setup\n * Handles initial OAuth flow and token management for Linear integration\n */\n\nimport { createHash, randomBytes } from 'crypto';\nimport { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { join } from 'path';\nimport { logger } from '../../core/monitoring/logger.js';\nimport { IntegrationError, ErrorCode } from '../../core/errors/index.js';\n\nexport interface LinearAuthConfig {\n clientId: string;\n clientSecret: string;\n redirectUri: string;\n scopes: string[];\n}\n\nexport interface LinearTokens {\n accessToken: string;\n refreshToken?: string;\n expiresAt: number; // Unix timestamp\n scope: string[];\n}\n\nexport interface LinearAuthResult {\n accessToken: string;\n refreshToken?: string;\n expiresIn: number;\n scope: string;\n tokenType: string;\n}\n\nexport class LinearAuthManager {\n private configPath: string;\n private tokensPath: string;\n private config?: LinearAuthConfig;\n\n constructor(projectRoot: string) {\n const configDir = join(projectRoot, '.stackmemory');\n this.configPath = join(configDir, 'linear-config.json');\n this.tokensPath = join(configDir, 'linear-tokens.json');\n }\n\n /**\n * Check if Linear integration is configured\n */\n isConfigured(): boolean {\n return existsSync(this.configPath) && existsSync(this.tokensPath);\n }\n\n /**\n * Save OAuth application configuration\n */\n saveConfig(config: LinearAuthConfig): void {\n writeFileSync(this.configPath, JSON.stringify(config, null, 2));\n this.config = config;\n logger.info('Linear OAuth configuration saved');\n }\n\n /**\n * Load OAuth configuration\n */\n loadConfig(): LinearAuthConfig | null {\n if (!existsSync(this.configPath)) {\n return null;\n }\n\n try {\n const configData = readFileSync(this.configPath, 'utf8');\n this.config = JSON.parse(configData);\n return this.config!;\n } catch (error: unknown) {\n logger.error('Failed to load Linear configuration:', error as Error);\n return null;\n }\n }\n\n /**\n * Generate OAuth authorization URL with PKCE\n */\n generateAuthUrl(state?: string): { url: string; codeVerifier: string } {\n if (!this.config) {\n throw new IntegrationError(\n 'Linear OAuth configuration not loaded',\n ErrorCode.LINEAR_AUTH_FAILED\n );\n }\n\n // Generate PKCE parameters\n const codeVerifier = randomBytes(32).toString('base64url');\n const codeChallenge = createHash('sha256')\n .update(codeVerifier)\n .digest('base64url');\n\n const params = new URLSearchParams({\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n response_type: 'code',\n scope: this.config.scopes.join(' '),\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n actor: 'app', // Enable actor authorization for service accounts\n });\n\n if (state) {\n params.set('state', state);\n }\n\n const authUrl = `https://linear.app/oauth/authorize?${params.toString()}`;\n\n return { url: authUrl, codeVerifier };\n }\n\n /**\n * Exchange authorization code for access token\n */\n async exchangeCodeForToken(\n authCode: string,\n codeVerifier: string\n ): Promise<LinearTokens> {\n if (!this.config) {\n throw new IntegrationError(\n 'Linear OAuth configuration not loaded',\n ErrorCode.LINEAR_AUTH_FAILED\n );\n }\n\n const tokenUrl = 'https://api.linear.app/oauth/token';\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: this.config.clientId,\n client_secret: this.config.clientSecret,\n redirect_uri: this.config.redirectUri,\n code: authCode,\n code_verifier: codeVerifier,\n });\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new IntegrationError(\n `Token exchange failed: ${response.status}`,\n ErrorCode.LINEAR_AUTH_FAILED,\n { status: response.status, body: errorText }\n );\n }\n\n const result = (await response.json()) as LinearAuthResult;\n\n // Calculate expiration time (tokens expire in 24 hours)\n const expiresAt = Date.now() + result.expiresIn * 1000;\n\n const tokens: LinearTokens = {\n accessToken: result.accessToken,\n refreshToken: result.refreshToken,\n expiresAt,\n scope: result.scope.split(' '),\n };\n\n this.saveTokens(tokens);\n return tokens;\n }\n\n /**\n * Refresh access token using refresh token\n */\n async refreshAccessToken(): Promise<LinearTokens> {\n if (!this.config) {\n throw new IntegrationError(\n 'Linear OAuth configuration not loaded',\n ErrorCode.LINEAR_AUTH_FAILED\n );\n }\n\n const currentTokens = this.loadTokens();\n if (!currentTokens?.refreshToken) {\n throw new IntegrationError(\n 'No refresh token available',\n ErrorCode.LINEAR_AUTH_FAILED\n );\n }\n\n const tokenUrl = 'https://api.linear.app/oauth/token';\n\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: this.config.clientId,\n client_secret: this.config.clientSecret,\n refresh_token: currentTokens.refreshToken,\n });\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new IntegrationError(\n `Token refresh failed: ${response.status}`,\n ErrorCode.LINEAR_AUTH_FAILED,\n { status: response.status, body: errorText }\n );\n }\n\n const result = (await response.json()) as LinearAuthResult;\n\n const tokens: LinearTokens = {\n accessToken: result.accessToken,\n refreshToken: result.refreshToken || currentTokens.refreshToken,\n expiresAt: Date.now() + result.expiresIn * 1000,\n scope: result.scope.split(' '),\n };\n\n this.saveTokens(tokens);\n return tokens;\n }\n\n /**\n * Get valid access token (refresh if needed)\n */\n async getValidToken(): Promise<string> {\n const tokens = this.loadTokens();\n if (!tokens) {\n throw new IntegrationError(\n 'No Linear tokens found. Please complete OAuth setup.',\n ErrorCode.LINEAR_AUTH_FAILED\n );\n }\n\n // Check if token expires in next 5 minutes\n const fiveMinutes = 5 * 60 * 1000;\n if (tokens.expiresAt - Date.now() < fiveMinutes) {\n logger.info('Linear token expiring soon, refreshing...');\n const newTokens = await this.refreshAccessToken();\n return newTokens.accessToken;\n }\n\n return tokens.accessToken;\n }\n\n /**\n * Save tokens to file\n */\n private saveTokens(tokens: LinearTokens): void {\n writeFileSync(this.tokensPath, JSON.stringify(tokens, null, 2));\n logger.info('Linear tokens saved');\n }\n\n /**\n * Load tokens from file\n */\n loadTokens(): LinearTokens | null {\n if (!existsSync(this.tokensPath)) {\n return null;\n }\n\n try {\n const tokensData = readFileSync(this.tokensPath, 'utf8');\n return JSON.parse(tokensData);\n } catch (error: unknown) {\n logger.error('Failed to load Linear tokens:', error as Error);\n return null;\n }\n }\n\n /**\n * Clear stored tokens and config\n */\n clearAuth(): void {\n if (existsSync(this.tokensPath)) {\n writeFileSync(this.tokensPath, '');\n }\n if (existsSync(this.configPath)) {\n writeFileSync(this.configPath, '');\n }\n logger.info('Linear authentication cleared');\n }\n}\n\n/**\n * Default Linear OAuth scopes for task management\n */\nexport const DEFAULT_LINEAR_SCOPES = [\n 'read', // Read issues, projects, teams\n 'write', // Create and update issues\n 'admin', // Manage team settings and workflows\n];\n\n/**\n * Linear OAuth setup helper\n */\nexport class LinearOAuthSetup {\n private authManager: LinearAuthManager;\n\n constructor(projectRoot: string) {\n this.authManager = new LinearAuthManager(projectRoot);\n }\n\n /**\n * Interactive setup for Linear OAuth\n */\n async setupInteractive(): Promise<{\n authUrl: string;\n instructions: string[];\n }> {\n // For now, we'll provide manual setup instructions\n // In a full implementation, this could open a browser or use a local server\n\n const config: LinearAuthConfig = {\n clientId: process.env['LINEAR_CLIENT_ID'] || '',\n clientSecret: process.env['LINEAR_CLIENT_SECRET'] || '',\n redirectUri:\n process.env['LINEAR_REDIRECT_URI'] ||\n 'http://localhost:3456/auth/linear/callback',\n scopes: DEFAULT_LINEAR_SCOPES,\n };\n\n if (!config.clientId || !config.clientSecret) {\n return {\n authUrl: '',\n instructions: [\n '1. Create a Linear OAuth application at https://linear.app/settings/api',\n '2. Set redirect URI to: http://localhost:3456/auth/linear/callback',\n '3. Copy your Client ID and Client Secret',\n '4. Set environment variables:',\n ' export LINEAR_CLIENT_ID=\"your_client_id\"',\n ' export LINEAR_CLIENT_SECRET=\"your_client_secret\"',\n '5. Re-run this setup command',\n ],\n };\n }\n\n this.authManager.saveConfig(config);\n\n const { url, codeVerifier } = this.authManager.generateAuthUrl();\n\n // Store code verifier temporarily (in a real app, this would be in a secure session store)\n process.env['_LINEAR_CODE_VERIFIER'] = codeVerifier;\n\n return {\n authUrl: url,\n instructions: [\n '1. Open this URL in your browser:',\n url,\n '',\n '2. Approve the StackMemory integration',\n '3. Copy the authorization code from the redirect URL',\n '4. Run: stackmemory linear authorize <code>',\n ],\n };\n }\n\n /**\n * Complete OAuth flow with authorization code\n */\n async completeAuth(authCode: string): Promise<boolean> {\n try {\n const codeVerifier = process.env['_LINEAR_CODE_VERIFIER'];\n if (!codeVerifier) {\n throw new IntegrationError(\n 'Code verifier not found. Please restart the setup process.',\n ErrorCode.LINEAR_AUTH_FAILED\n );\n }\n\n await this.authManager.exchangeCodeForToken(authCode, codeVerifier);\n delete process.env['_LINEAR_CODE_VERIFIER'];\n\n logger.info('Linear OAuth setup completed successfully!');\n return true;\n } catch (error: unknown) {\n logger.error('Failed to complete Linear OAuth setup:', error as Error);\n return false;\n }\n }\n\n /**\n * Test the Linear connection\n */\n async testConnection(): Promise<boolean> {\n try {\n const token = await this.authManager.getValidToken();\n\n // Test with a simple GraphQL query\n const response = await fetch('https://api.linear.app/graphql', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n query: 'query { viewer { id name email } }',\n }),\n });\n\n if (response.ok) {\n const result = (await response.json()) as {\n data?: { viewer?: { id: string; name: string; email: string } };\n };\n if (result.data?.viewer) {\n logger.info(\n `Connected to Linear as: ${result.data.viewer.name} (${result.data.viewer.email})`\n );\n return true;\n }\n }\n\n return false;\n } catch (error: unknown) {\n logger.error('Linear connection test failed:', error as Error);\n return false;\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,6 +7,7 @@ import { LinearTaskManager } from "../../features/tasks/linear-task-manager.js";
|
|
|
7
7
|
import { LinearAuthManager } from "./auth.js";
|
|
8
8
|
import { LinearSyncEngine, DEFAULT_SYNC_CONFIG } from "./sync.js";
|
|
9
9
|
import { LinearConfigManager } from "./config.js";
|
|
10
|
+
import { IntegrationError, ErrorCode } from "../../core/errors/index.js";
|
|
10
11
|
import Database from "better-sqlite3";
|
|
11
12
|
import { join } from "path";
|
|
12
13
|
import { existsSync } from "fs";
|
|
@@ -50,14 +51,16 @@ class LinearAutoSyncService {
|
|
|
50
51
|
try {
|
|
51
52
|
const authManager = new LinearAuthManager(this.projectRoot);
|
|
52
53
|
if (!authManager.isConfigured()) {
|
|
53
|
-
throw new
|
|
54
|
-
'Linear integration not configured. Run "stackmemory linear setup" first.'
|
|
54
|
+
throw new IntegrationError(
|
|
55
|
+
'Linear integration not configured. Run "stackmemory linear setup" first.',
|
|
56
|
+
ErrorCode.LINEAR_AUTH_FAILED
|
|
55
57
|
);
|
|
56
58
|
}
|
|
57
59
|
const dbPath = join(this.projectRoot, ".stackmemory", "context.db");
|
|
58
60
|
if (!existsSync(dbPath)) {
|
|
59
|
-
throw new
|
|
60
|
-
'StackMemory not initialized. Run "stackmemory init" first.'
|
|
61
|
+
throw new IntegrationError(
|
|
62
|
+
'StackMemory not initialized. Run "stackmemory init" first.',
|
|
63
|
+
ErrorCode.LINEAR_SYNC_FAILED
|
|
61
64
|
);
|
|
62
65
|
}
|
|
63
66
|
const db = new Database(dbPath);
|
|
@@ -69,8 +72,9 @@ class LinearAutoSyncService {
|
|
|
69
72
|
);
|
|
70
73
|
const token = await authManager.getValidToken();
|
|
71
74
|
if (!token) {
|
|
72
|
-
throw new
|
|
73
|
-
"Unable to get valid Linear token. Check authentication."
|
|
75
|
+
throw new IntegrationError(
|
|
76
|
+
"Unable to get valid Linear token. Check authentication.",
|
|
77
|
+
ErrorCode.LINEAR_AUTH_FAILED
|
|
74
78
|
);
|
|
75
79
|
}
|
|
76
80
|
this.isRunning = true;
|
|
@@ -126,7 +130,10 @@ class LinearAutoSyncService {
|
|
|
126
130
|
*/
|
|
127
131
|
async forceSync() {
|
|
128
132
|
if (!this.syncEngine) {
|
|
129
|
-
throw new
|
|
133
|
+
throw new IntegrationError(
|
|
134
|
+
"Sync engine not initialized",
|
|
135
|
+
ErrorCode.LINEAR_SYNC_FAILED
|
|
136
|
+
);
|
|
130
137
|
}
|
|
131
138
|
logger.info("Forcing immediate Linear sync");
|
|
132
139
|
await this.performSync();
|
|
@@ -183,7 +190,10 @@ class LinearAutoSyncService {
|
|
|
183
190
|
});
|
|
184
191
|
}
|
|
185
192
|
} else {
|
|
186
|
-
throw new
|
|
193
|
+
throw new IntegrationError(
|
|
194
|
+
`Sync failed: ${result.errors.join(", ")}`,
|
|
195
|
+
ErrorCode.LINEAR_SYNC_FAILED
|
|
196
|
+
);
|
|
187
197
|
}
|
|
188
198
|
} catch (error) {
|
|
189
199
|
logger.error("Linear auto-sync failed:", error);
|