@stackmemoryai/stackmemory 0.5.38 → 0.5.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ class AsyncMutex {
6
+ locked = false;
7
+ waiting = [];
8
+ lockHolder = null;
9
+ lockAcquiredAt = 0;
10
+ lockTimeout;
11
+ constructor(lockTimeoutMs = 3e5) {
12
+ this.lockTimeout = lockTimeoutMs;
13
+ }
14
+ /**
15
+ * Acquire the lock. Waits if already locked.
16
+ * Returns a release function that MUST be called when done.
17
+ */
18
+ async acquire(holder) {
19
+ if (this.locked && this.lockAcquiredAt > 0) {
20
+ const elapsed = Date.now() - this.lockAcquiredAt;
21
+ if (elapsed > this.lockTimeout) {
22
+ console.warn(
23
+ `[AsyncMutex] Stale lock detected (held by ${this.lockHolder} for ${elapsed}ms), forcing release`
24
+ );
25
+ this.forceRelease();
26
+ }
27
+ }
28
+ if (!this.locked) {
29
+ this.locked = true;
30
+ this.lockHolder = holder || "unknown";
31
+ this.lockAcquiredAt = Date.now();
32
+ return () => this.release();
33
+ }
34
+ return new Promise((resolve) => {
35
+ this.waiting.push(() => {
36
+ this.locked = true;
37
+ this.lockHolder = holder || "unknown";
38
+ this.lockAcquiredAt = Date.now();
39
+ resolve(() => this.release());
40
+ });
41
+ });
42
+ }
43
+ /**
44
+ * Try to acquire the lock without waiting
45
+ * Returns release function if acquired, null if already locked
46
+ */
47
+ tryAcquire(holder) {
48
+ if (this.locked && this.lockAcquiredAt > 0) {
49
+ const elapsed = Date.now() - this.lockAcquiredAt;
50
+ if (elapsed > this.lockTimeout) {
51
+ console.warn(
52
+ `[AsyncMutex] Stale lock detected (held by ${this.lockHolder} for ${elapsed}ms), forcing release`
53
+ );
54
+ this.forceRelease();
55
+ }
56
+ }
57
+ if (!this.locked) {
58
+ this.locked = true;
59
+ this.lockHolder = holder || "unknown";
60
+ this.lockAcquiredAt = Date.now();
61
+ return () => this.release();
62
+ }
63
+ return null;
64
+ }
65
+ release() {
66
+ const next = this.waiting.shift();
67
+ if (next) {
68
+ next();
69
+ } else {
70
+ this.locked = false;
71
+ this.lockHolder = null;
72
+ this.lockAcquiredAt = 0;
73
+ }
74
+ }
75
+ forceRelease() {
76
+ this.locked = false;
77
+ this.lockHolder = null;
78
+ this.lockAcquiredAt = 0;
79
+ }
80
+ /**
81
+ * Execute a function while holding the lock
82
+ */
83
+ async withLock(fn, holder) {
84
+ const release = await this.acquire(holder);
85
+ try {
86
+ return await fn();
87
+ } finally {
88
+ release();
89
+ }
90
+ }
91
+ /**
92
+ * Check if currently locked
93
+ */
94
+ isLocked() {
95
+ return this.locked;
96
+ }
97
+ /**
98
+ * Get current lock status
99
+ */
100
+ getStatus() {
101
+ return {
102
+ locked: this.locked,
103
+ holder: this.lockHolder,
104
+ acquiredAt: this.lockAcquiredAt,
105
+ waitingCount: this.waiting.length
106
+ };
107
+ }
108
+ }
109
+ const syncMutex = new AsyncMutex(3e5);
110
+ export {
111
+ AsyncMutex,
112
+ syncMutex
113
+ };
114
+ //# sourceMappingURL=async-mutex.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/core/utils/async-mutex.ts"],
4
+ "sourcesContent": ["/**\n * Simple async mutex implementation for thread-safe operations\n * Prevents race conditions when multiple async operations compete for the same resource\n */\n\nexport class AsyncMutex {\n private locked = false;\n private waiting: Array<() => void> = [];\n private lockHolder: string | null = null;\n private lockAcquiredAt: number = 0;\n private readonly lockTimeout: number;\n\n constructor(lockTimeoutMs: number = 300000) {\n // Default 5 minute timeout\n this.lockTimeout = lockTimeoutMs;\n }\n\n /**\n * Acquire the lock. Waits if already locked.\n * Returns a release function that MUST be called when done.\n */\n async acquire(holder?: string): Promise<() => void> {\n // Check for stale lock (lock timeout)\n if (this.locked && this.lockAcquiredAt > 0) {\n const elapsed = Date.now() - this.lockAcquiredAt;\n if (elapsed > this.lockTimeout) {\n console.warn(\n `[AsyncMutex] Stale lock detected (held by ${this.lockHolder} for ${elapsed}ms), forcing release`\n );\n this.forceRelease();\n }\n }\n\n if (!this.locked) {\n this.locked = true;\n this.lockHolder = holder || 'unknown';\n this.lockAcquiredAt = Date.now();\n return () => this.release();\n }\n\n // Wait for lock to be released\n return new Promise((resolve) => {\n this.waiting.push(() => {\n this.locked = true;\n this.lockHolder = holder || 'unknown';\n this.lockAcquiredAt = Date.now();\n resolve(() => this.release());\n });\n });\n }\n\n /**\n * Try to acquire the lock without waiting\n * Returns release function if acquired, null if already locked\n */\n tryAcquire(holder?: string): (() => void) | null {\n // Check for stale lock\n if (this.locked && this.lockAcquiredAt > 0) {\n const elapsed = Date.now() - this.lockAcquiredAt;\n if (elapsed > this.lockTimeout) {\n console.warn(\n `[AsyncMutex] Stale lock detected (held by ${this.lockHolder} for ${elapsed}ms), forcing release`\n );\n this.forceRelease();\n }\n }\n\n if (!this.locked) {\n this.locked = true;\n this.lockHolder = holder || 'unknown';\n this.lockAcquiredAt = Date.now();\n return () => this.release();\n }\n return null;\n }\n\n private release(): void {\n const next = this.waiting.shift();\n if (next) {\n next();\n } else {\n this.locked = false;\n this.lockHolder = null;\n this.lockAcquiredAt = 0;\n }\n }\n\n private forceRelease(): void {\n this.locked = false;\n this.lockHolder = null;\n this.lockAcquiredAt = 0;\n }\n\n /**\n * Execute a function while holding the lock\n */\n async withLock<T>(fn: () => Promise<T>, holder?: string): Promise<T> {\n const release = await this.acquire(holder);\n try {\n return await fn();\n } finally {\n release();\n }\n }\n\n /**\n * Check if currently locked\n */\n isLocked(): boolean {\n return this.locked;\n }\n\n /**\n * Get current lock status\n */\n getStatus(): {\n locked: boolean;\n holder: string | null;\n acquiredAt: number;\n waitingCount: number;\n } {\n return {\n locked: this.locked,\n holder: this.lockHolder,\n acquiredAt: this.lockAcquiredAt,\n waitingCount: this.waiting.length,\n };\n }\n}\n\n// Singleton instance for sync operations\nexport const syncMutex = new AsyncMutex(300000); // 5 minute timeout\n"],
5
+ "mappings": ";;;;AAKO,MAAM,WAAW;AAAA,EACd,SAAS;AAAA,EACT,UAA6B,CAAC;AAAA,EAC9B,aAA4B;AAAA,EAC5B,iBAAyB;AAAA,EAChB;AAAA,EAEjB,YAAY,gBAAwB,KAAQ;AAE1C,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAQ,QAAsC;AAElD,QAAI,KAAK,UAAU,KAAK,iBAAiB,GAAG;AAC1C,YAAM,UAAU,KAAK,IAAI,IAAI,KAAK;AAClC,UAAI,UAAU,KAAK,aAAa;AAC9B,gBAAQ;AAAA,UACN,6CAA6C,KAAK,UAAU,QAAQ,OAAO;AAAA,QAC7E;AACA,aAAK,aAAa;AAAA,MACpB;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,QAAQ;AAChB,WAAK,SAAS;AACd,WAAK,aAAa,UAAU;AAC5B,WAAK,iBAAiB,KAAK,IAAI;AAC/B,aAAO,MAAM,KAAK,QAAQ;AAAA,IAC5B;AAGA,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,WAAK,QAAQ,KAAK,MAAM;AACtB,aAAK,SAAS;AACd,aAAK,aAAa,UAAU;AAC5B,aAAK,iBAAiB,KAAK,IAAI;AAC/B,gBAAQ,MAAM,KAAK,QAAQ,CAAC;AAAA,MAC9B,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,WAAW,QAAsC;AAE/C,QAAI,KAAK,UAAU,KAAK,iBAAiB,GAAG;AAC1C,YAAM,UAAU,KAAK,IAAI,IAAI,KAAK;AAClC,UAAI,UAAU,KAAK,aAAa;AAC9B,gBAAQ;AAAA,UACN,6CAA6C,KAAK,UAAU,QAAQ,OAAO;AAAA,QAC7E;AACA,aAAK,aAAa;AAAA,MACpB;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,QAAQ;AAChB,WAAK,SAAS;AACd,WAAK,aAAa,UAAU;AAC5B,WAAK,iBAAiB,KAAK,IAAI;AAC/B,aAAO,MAAM,KAAK,QAAQ;AAAA,IAC5B;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,UAAgB;AACtB,UAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,QAAI,MAAM;AACR,WAAK;AAAA,IACP,OAAO;AACL,WAAK,SAAS;AACd,WAAK,aAAa;AAClB,WAAK,iBAAiB;AAAA,IACxB;AAAA,EACF;AAAA,EAEQ,eAAqB;AAC3B,SAAK,SAAS;AACd,SAAK,aAAa;AAClB,SAAK,iBAAiB;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAY,IAAsB,QAA6B;AACnE,UAAM,UAAU,MAAM,KAAK,QAAQ,MAAM;AACzC,QAAI;AACF,aAAO,MAAM,GAAG;AAAA,IAClB,UAAE;AACA,cAAQ;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,WAAoB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,YAKE;AACA,WAAO;AAAA,MACL,QAAQ,KAAK;AAAA,MACb,QAAQ,KAAK;AAAA,MACb,YAAY,KAAK;AAAA,MACjB,cAAc,KAAK,QAAQ;AAAA,IAC7B;AAAA,EACF;AACF;AAGO,MAAM,YAAY,IAAI,WAAW,GAAM;",
6
+ "names": []
7
+ }
@@ -2,15 +2,35 @@ import { fileURLToPath as __fileURLToPath } from 'url';
2
2
  import { dirname as __pathDirname } from 'path';
3
3
  const __filename = __fileURLToPath(import.meta.url);
4
4
  const __dirname = __pathDirname(__filename);
5
- import { writeFileSync, mkdirSync, chmodSync, existsSync } from "fs";
6
- import { dirname } from "path";
5
+ import {
6
+ writeFileSync,
7
+ mkdirSync,
8
+ chmodSync,
9
+ existsSync,
10
+ renameSync,
11
+ unlinkSync
12
+ } from "fs";
13
+ import { dirname, join } from "path";
14
+ import { randomBytes } from "crypto";
7
15
  function writeFileSecure(filePath, data) {
8
16
  const dir = dirname(filePath);
9
17
  if (!existsSync(dir)) {
10
18
  mkdirSync(dir, { recursive: true, mode: 448 });
11
19
  }
12
- writeFileSync(filePath, data);
13
- chmodSync(filePath, 384);
20
+ const tempPath = join(dir, `.tmp-${randomBytes(8).toString("hex")}`);
21
+ try {
22
+ writeFileSync(tempPath, data);
23
+ chmodSync(tempPath, 384);
24
+ renameSync(tempPath, filePath);
25
+ } catch (error) {
26
+ try {
27
+ if (existsSync(tempPath)) {
28
+ unlinkSync(tempPath);
29
+ }
30
+ } catch {
31
+ }
32
+ throw error;
33
+ }
14
34
  }
15
35
  function ensureSecureDir(dirPath) {
16
36
  if (!existsSync(dirPath)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/hooks/secure-fs.ts"],
4
- "sourcesContent": ["/**\n * Secure file system utilities for hooks\n * Ensures config files have restricted permissions (0600)\n */\n\nimport { writeFileSync, mkdirSync, chmodSync, existsSync } from 'fs';\nimport { dirname } from 'path';\n\n/**\n * Write file with secure permissions (0600 - user read/write only)\n * Also ensures parent directory has 0700 permissions\n */\nexport function writeFileSecure(filePath: string, data: string): void {\n const dir = dirname(filePath);\n\n // Create directory with secure permissions if needed\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n\n // Write file\n writeFileSync(filePath, data);\n\n // Set secure permissions (user read/write only)\n chmodSync(filePath, 0o600);\n}\n\n/**\n * Ensure directory exists with secure permissions (0700)\n */\nexport function ensureSecureDir(dirPath: string): void {\n if (!existsSync(dirPath)) {\n mkdirSync(dirPath, { recursive: true, mode: 0o700 });\n } else {\n // Set permissions on existing directory\n try {\n chmodSync(dirPath, 0o700);\n } catch {\n // Ignore if we can't change permissions (not owner)\n }\n }\n}\n"],
5
- "mappings": ";;;;AAKA,SAAS,eAAe,WAAW,WAAW,kBAAkB;AAChE,SAAS,eAAe;AAMjB,SAAS,gBAAgB,UAAkB,MAAoB;AACpE,QAAM,MAAM,QAAQ,QAAQ;AAG5B,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,cAAU,KAAK,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAAA,EACjD;AAGA,gBAAc,UAAU,IAAI;AAG5B,YAAU,UAAU,GAAK;AAC3B;AAKO,SAAS,gBAAgB,SAAuB;AACrD,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,cAAU,SAAS,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAAA,EACrD,OAAO;AAEL,QAAI;AACF,gBAAU,SAAS,GAAK;AAAA,IAC1B,QAAQ;AAAA,IAER;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["/**\n * Secure file system utilities for hooks\n * Ensures config files have restricted permissions (0600)\n */\n\nimport {\n writeFileSync,\n mkdirSync,\n chmodSync,\n existsSync,\n renameSync,\n unlinkSync,\n} from 'fs';\nimport { dirname, join } from 'path';\nimport { randomBytes } from 'crypto';\n\n/**\n * Write file with secure permissions (0600 - user read/write only)\n * Uses atomic write pattern: write to temp file, then rename\n * This prevents corruption if process crashes mid-write\n */\nexport function writeFileSecure(filePath: string, data: string): void {\n const dir = dirname(filePath);\n\n // Create directory with secure permissions if needed\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n\n // Generate temp file path in same directory (required for atomic rename)\n const tempPath = join(dir, `.tmp-${randomBytes(8).toString('hex')}`);\n\n try {\n // Write to temp file first\n writeFileSync(tempPath, data);\n\n // Set secure permissions on temp file\n chmodSync(tempPath, 0o600);\n\n // Atomic rename (same filesystem, so this is atomic on POSIX)\n renameSync(tempPath, filePath);\n } catch (error) {\n // Clean up temp file on failure\n try {\n if (existsSync(tempPath)) {\n unlinkSync(tempPath);\n }\n } catch {\n // Ignore cleanup errors\n }\n throw error;\n }\n}\n\n/**\n * Ensure directory exists with secure permissions (0700)\n */\nexport function ensureSecureDir(dirPath: string): void {\n if (!existsSync(dirPath)) {\n mkdirSync(dirPath, { recursive: true, mode: 0o700 });\n } else {\n // Set permissions on existing directory\n try {\n chmodSync(dirPath, 0o700);\n } catch {\n // Ignore if we can't change permissions (not owner)\n }\n }\n}\n"],
5
+ "mappings": ";;;;AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS,YAAY;AAC9B,SAAS,mBAAmB;AAOrB,SAAS,gBAAgB,UAAkB,MAAoB;AACpE,QAAM,MAAM,QAAQ,QAAQ;AAG5B,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,cAAU,KAAK,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAAA,EACjD;AAGA,QAAM,WAAW,KAAK,KAAK,QAAQ,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC,EAAE;AAEnE,MAAI;AAEF,kBAAc,UAAU,IAAI;AAG5B,cAAU,UAAU,GAAK;AAGzB,eAAW,UAAU,QAAQ;AAAA,EAC/B,SAAS,OAAO;AAEd,QAAI;AACF,UAAI,WAAW,QAAQ,GAAG;AACxB,mBAAW,QAAQ;AAAA,MACrB;AAAA,IACF,QAAQ;AAAA,IAER;AACA,UAAM;AAAA,EACR;AACF;AAKO,SAAS,gBAAgB,SAAuB;AACrD,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,cAAU,SAAS,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAAA,EACrD,OAAO;AAEL,QAAI;AACF,gBAAU,SAAS,GAAK;AAAA,IAC1B,QAAQ;AAAA,IAER;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/hooks/sms-action-runner.ts"],
4
- "sourcesContent": ["/**\n * SMS Action Runner - Executes actions based on SMS responses\n * Bridges SMS responses to Claude Code actions\n *\n * Security: Uses allowlist-based action execution to prevent command injection\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { execFileSync } from 'child_process';\nimport { randomBytes } from 'crypto';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport { ActionQueueSchema, parseConfigSafe } from './schemas.js';\nimport { LinearClient } from '../integrations/linear/client.js';\nimport { LinearAuthManager } from '../integrations/linear/auth.js';\n\n/**\n * Parse a command string into an array of arguments, respecting quotes\n * Handles single quotes, double quotes, and escaped characters\n *\n * Examples:\n * 'echo \"hello world\"' -> ['echo', 'hello world']\n * \"git commit -m 'fix bug'\" -> ['git', 'commit', '-m', 'fix bug']\n * 'npm run build' -> ['npm', 'run', 'build']\n */\nfunction parseCommandArgs(command: string): string[] {\n const args: string[] = [];\n let current = '';\n let inSingleQuote = false;\n let inDoubleQuote = false;\n let escaped = false;\n\n for (let i = 0; i < command.length; i++) {\n const char = command[i];\n\n if (escaped) {\n current += char;\n escaped = false;\n continue;\n }\n\n if (char === '\\\\' && !inSingleQuote) {\n escaped = true;\n continue;\n }\n\n if (char === \"'\" && !inDoubleQuote) {\n inSingleQuote = !inSingleQuote;\n continue;\n }\n\n if (char === '\"' && !inSingleQuote) {\n inDoubleQuote = !inDoubleQuote;\n continue;\n }\n\n if (char === ' ' && !inSingleQuote && !inDoubleQuote) {\n if (current.length > 0) {\n args.push(current);\n current = '';\n }\n continue;\n }\n\n current += char;\n }\n\n if (current.length > 0) {\n args.push(current);\n }\n\n return args;\n}\n\n// Allowlist of safe action patterns\nconst SAFE_ACTION_PATTERNS: Array<{\n pattern: RegExp;\n validate?: (match: RegExpMatchArray) => boolean;\n}> = [\n // Git/GitHub CLI commands (limited to safe operations)\n { pattern: /^gh pr (view|list|status|checks) (\\d+)$/ },\n { pattern: /^gh pr review (\\d+) --approve$/ },\n { pattern: /^gh pr merge (\\d+) --squash$/ },\n { pattern: /^gh issue (view|list) (\\d+)?$/ },\n\n // NPM commands (limited to safe operations)\n { pattern: /^npm run (build|test|lint|lint:fix|test:run)$/ },\n { pattern: /^npm (test|run build)$/ },\n\n // StackMemory commands\n { pattern: /^stackmemory (status|notify check|context list)$/ },\n // Task start with optional --assign-me flag (Linear task ID is UUID format)\n {\n pattern: /^stackmemory task start ([a-f0-9-]{36})( --assign-me)?$/,\n },\n // Additional StackMemory commands for mobile/WhatsApp\n { pattern: /^stackmemory context show$/ },\n { pattern: /^stackmemory task list$/ },\n\n // Git commands\n { pattern: /^git (status|diff|log|branch)( --[a-z-]+)*$/ },\n { pattern: /^git add -A && git commit$/ },\n { pattern: /^gh pr create --fill$/ },\n // Git log with line limit for mobile-friendly output\n { pattern: /^git log --oneline -\\d{1,2}$/ },\n\n // WhatsApp/Mobile quick commands\n { pattern: /^status$/i },\n { pattern: /^tasks$/i },\n { pattern: /^context$/i },\n { pattern: /^help$/i },\n { pattern: /^sync$/i },\n\n // Claude Code launcher\n { pattern: /^claude-sm$/ },\n\n // Log viewing (safe read-only)\n { pattern: /^tail -\\d+ ~\\/\\.claude\\/logs\\/\\*\\.log$/ },\n\n // Custom aliases (cwm = claude worktree merge)\n { pattern: /^cwm$/ },\n\n // Simple echo/confirmation (no variables)\n {\n pattern:\n /^echo \"?(Done|OK|Confirmed|Acknowledged|Great work! Time for a coffee break\\.)\"?$/,\n },\n];\n\n/**\n * Check if an action is in the allowlist\n */\nfunction isActionAllowed(action: string): boolean {\n const trimmed = action.trim();\n return SAFE_ACTION_PATTERNS.some(({ pattern, validate }) => {\n const match = trimmed.match(pattern);\n if (!match) return false;\n if (validate && !validate(match)) return false;\n return true;\n });\n}\n\nexport interface PendingAction {\n id: string;\n promptId: string;\n response: string;\n action: string;\n timestamp: string;\n status: 'pending' | 'running' | 'completed' | 'failed';\n result?: string;\n error?: string;\n}\n\nexport interface ActionQueue {\n actions: PendingAction[];\n lastChecked: string;\n}\n\nconst QUEUE_PATH = join(homedir(), '.stackmemory', 'sms-action-queue.json');\n\nconst DEFAULT_QUEUE: ActionQueue = {\n actions: [],\n lastChecked: new Date().toISOString(),\n};\n\nexport function loadActionQueue(): ActionQueue {\n try {\n if (existsSync(QUEUE_PATH)) {\n const data = JSON.parse(readFileSync(QUEUE_PATH, 'utf8'));\n return parseConfigSafe(\n ActionQueueSchema,\n data,\n DEFAULT_QUEUE,\n 'action-queue'\n );\n }\n } catch {\n // Use defaults\n }\n return { ...DEFAULT_QUEUE, lastChecked: new Date().toISOString() };\n}\n\nexport function saveActionQueue(queue: ActionQueue): void {\n try {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n writeFileSecure(QUEUE_PATH, JSON.stringify(queue, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nexport function queueAction(\n promptId: string,\n response: string,\n action: string\n): string {\n const queue = loadActionQueue();\n // Use cryptographically secure random ID\n const id = randomBytes(8).toString('hex');\n\n queue.actions.push({\n id,\n promptId,\n response,\n action,\n timestamp: new Date().toISOString(),\n status: 'pending',\n });\n\n saveActionQueue(queue);\n return id;\n}\n\n/**\n * Get Linear client if available\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 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 * Handle special actions that require API calls instead of shell commands\n */\nasync function handleSpecialAction(action: string): Promise<{\n handled: boolean;\n success?: boolean;\n output?: string;\n error?: string;\n}> {\n // Handle stackmemory task start command\n const taskStartMatch = action.match(\n /^stackmemory task start ([a-f0-9-]{36})( --assign-me)?$/\n );\n if (taskStartMatch) {\n const issueId = taskStartMatch[1];\n const client = getLinearClient();\n\n if (!client) {\n return {\n handled: true,\n success: false,\n error:\n 'Linear not configured. Set LINEAR_API_KEY or run stackmemory linear setup.',\n };\n }\n\n try {\n const result = await client.startIssue(issueId);\n if (result.success && result.issue) {\n return {\n handled: true,\n success: true,\n output: `Started: ${result.issue.identifier} - ${result.issue.title}`,\n };\n }\n return {\n handled: true,\n success: false,\n error: result.error || 'Failed to start issue',\n };\n } catch (err) {\n return {\n handled: true,\n success: false,\n error: err instanceof Error ? err.message : 'Unknown error',\n };\n }\n }\n\n return { handled: false };\n}\n\n/**\n * Execute an action safely using allowlist validation\n * This prevents command injection by only allowing pre-approved commands\n */\nexport async function executeActionSafe(\n action: string,\n _response: string\n): Promise<{ success: boolean; output?: string; error?: string }> {\n // Check if action is in allowlist\n if (!isActionAllowed(action)) {\n console.error(`[sms-action] Action not in allowlist: ${action}`);\n return {\n success: false,\n error: `Action not allowed. Only pre-approved commands can be executed via SMS.`,\n };\n }\n\n // Check for special actions that need API calls\n const specialResult = await handleSpecialAction(action);\n if (specialResult.handled) {\n return {\n success: specialResult.success || false,\n output: specialResult.output,\n error: specialResult.error,\n };\n }\n\n try {\n console.log(`[sms-action] Executing safe action: ${action}`);\n\n // Parse the action into command and args, respecting quotes\n const parts = parseCommandArgs(action);\n if (parts.length === 0) {\n return { success: false, error: 'Empty command' };\n }\n\n const cmd = parts[0];\n const args = parts.slice(1);\n\n // Use execFileSync for commands without shell interpretation\n // This prevents shell injection even if the allowlist is somehow bypassed\n const output = execFileSync(cmd, args, {\n encoding: 'utf8',\n timeout: 60000,\n stdio: ['pipe', 'pipe', 'pipe'],\n shell: false, // Explicitly disable shell\n });\n\n return { success: true, output };\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n return { success: false, error };\n }\n}\n\nexport function getPendingActions(): PendingAction[] {\n const queue = loadActionQueue();\n return queue.actions.filter((a) => a.status === 'pending');\n}\n\nexport function markActionRunning(id: string): void {\n const queue = loadActionQueue();\n const action = queue.actions.find((a) => a.id === id);\n if (action) {\n action.status = 'running';\n saveActionQueue(queue);\n }\n}\n\nexport function markActionCompleted(\n id: string,\n result?: string,\n error?: string\n): void {\n const queue = loadActionQueue();\n const action = queue.actions.find((a) => a.id === id);\n if (action) {\n action.status = error ? 'failed' : 'completed';\n action.result = result;\n action.error = error;\n saveActionQueue(queue);\n }\n}\n\nexport async function executeAction(action: PendingAction): Promise<{\n success: boolean;\n output?: string;\n error?: string;\n}> {\n markActionRunning(action.id);\n\n // Use the safe execution path to prevent command injection\n const result = await executeActionSafe(action.action, action.response);\n\n if (result.success) {\n markActionCompleted(action.id, result.output);\n } else {\n markActionCompleted(action.id, undefined, result.error);\n }\n\n return result;\n}\n\nexport async function processAllPendingActions(): Promise<{\n processed: number;\n succeeded: number;\n failed: number;\n}> {\n const pending = getPendingActions();\n let succeeded = 0;\n let failed = 0;\n\n for (const action of pending) {\n const result = await executeAction(action);\n if (result.success) {\n succeeded++;\n } else {\n failed++;\n }\n }\n\n return { processed: pending.length, succeeded, failed };\n}\n\n// Clean up old completed actions (keep last 50)\nexport function cleanupOldActions(): number {\n const queue = loadActionQueue();\n const completed = queue.actions.filter(\n (a) => a.status === 'completed' || a.status === 'failed'\n );\n\n if (completed.length > 50) {\n const toRemove = completed.slice(0, completed.length - 50);\n queue.actions = queue.actions.filter(\n (a) => !toRemove.find((r) => r.id === a.id)\n );\n saveActionQueue(queue);\n return toRemove.length;\n }\n\n return 0;\n}\n\n/**\n * Action Templates - Common actions for SMS responses\n *\n * SECURITY NOTE: These templates return command strings that must be\n * validated against SAFE_ACTION_PATTERNS before execution.\n * Templates that accept user input are removed to prevent injection.\n */\nexport const ACTION_TEMPLATES = {\n // Git/PR actions (PR numbers must be validated as integers)\n approvePR: (prNumber: string) => {\n // Validate PR number is numeric only\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr review ${prNumber} --approve`;\n },\n mergePR: (prNumber: string) => {\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr merge ${prNumber} --squash`;\n },\n viewPR: (prNumber: string) => {\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr view ${prNumber}`;\n },\n\n // Build actions (no user input)\n rebuild: () => `npm run build`,\n retest: () => `npm run test:run`,\n lint: () => `npm run lint:fix`,\n\n // Status actions (no user input)\n status: () => `stackmemory status`,\n checkNotifications: () => `stackmemory notify check`,\n\n // REMOVED for security - these templates allowed arbitrary user input:\n // - requestChanges (allowed arbitrary message)\n // - closePR (could be used maliciously)\n // - deploy/rollback (too dangerous for SMS)\n // - verifyDeployment (allowed arbitrary URL)\n // - notifySlack (allowed arbitrary message - command injection)\n // - notifyTeam (allowed arbitrary message - command injection)\n};\n\n/**\n * Create action string from template\n */\nexport function createAction(\n template: keyof typeof ACTION_TEMPLATES,\n ...args: string[]\n): string {\n const fn = ACTION_TEMPLATES[template];\n if (typeof fn === 'function') {\n return (fn as (...args: string[]) => string)(...args);\n }\n return fn;\n}\n\n/**\n * Watch for new actions and execute them\n */\nexport function startActionWatcher(intervalMs: number = 5000): NodeJS.Timeout {\n console.log(\n `[sms-action] Starting action watcher (interval: ${intervalMs}ms)`\n );\n\n return setInterval(() => {\n const pending = getPendingActions();\n if (pending.length > 0) {\n console.log(`[sms-action] Found ${pending.length} pending action(s)`);\n processAllPendingActions();\n }\n }, intervalMs);\n}\n\n/**\n * Integration with SMS webhook - queue action when response received\n */\nexport function handleSMSResponse(\n promptId: string,\n response: string,\n action?: string\n): void {\n if (action) {\n const actionId = queueAction(promptId, response, action);\n console.log(`[sms-action] Queued action ${actionId}: ${action}`);\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * SMS Action Runner - Executes actions based on SMS responses\n * Bridges SMS responses to Claude Code actions\n *\n * Security: Uses allowlist-based action execution to prevent command injection\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { execFileSync } from 'child_process';\nimport { randomBytes } from 'crypto';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport { ActionQueueSchema, parseConfigSafe } from './schemas.js';\nimport { LinearClient } from '../integrations/linear/client.js';\nimport { LinearAuthManager } from '../integrations/linear/auth.js';\n\n/**\n * Parse a command string into an array of arguments, respecting quotes\n * Handles single quotes, double quotes, and escaped characters\n *\n * Examples:\n * 'echo \"hello world\"' -> ['echo', 'hello world']\n * \"git commit -m 'fix bug'\" -> ['git', 'commit', '-m', 'fix bug']\n * 'npm run build' -> ['npm', 'run', 'build']\n */\nfunction parseCommandArgs(command: string): string[] {\n const args: string[] = [];\n let current = '';\n let inSingleQuote = false;\n let inDoubleQuote = false;\n let escaped = false;\n\n for (let i = 0; i < command.length; i++) {\n const char = command[i];\n\n if (escaped) {\n current += char;\n escaped = false;\n continue;\n }\n\n if (char === '\\\\' && !inSingleQuote) {\n escaped = true;\n continue;\n }\n\n if (char === \"'\" && !inDoubleQuote) {\n inSingleQuote = !inSingleQuote;\n continue;\n }\n\n if (char === '\"' && !inSingleQuote) {\n inDoubleQuote = !inDoubleQuote;\n continue;\n }\n\n if (char === ' ' && !inSingleQuote && !inDoubleQuote) {\n if (current.length > 0) {\n args.push(current);\n current = '';\n }\n continue;\n }\n\n current += char;\n }\n\n if (current.length > 0) {\n args.push(current);\n }\n\n return args;\n}\n\n// Allowlist of safe action patterns\nconst SAFE_ACTION_PATTERNS: Array<{\n pattern: RegExp;\n validate?: (match: RegExpMatchArray) => boolean;\n}> = [\n // Git/GitHub CLI commands (limited to safe operations)\n { pattern: /^gh pr (view|list|status|checks) (\\d+)$/ },\n { pattern: /^gh pr review (\\d+) --approve$/ },\n { pattern: /^gh pr merge (\\d+) --squash$/ },\n { pattern: /^gh issue (view|list) (\\d+)?$/ },\n\n // NPM commands (limited to safe operations)\n { pattern: /^npm run (build|test|lint|lint:fix|test:run)$/ },\n { pattern: /^npm (test|run build)$/ },\n\n // StackMemory commands\n { pattern: /^stackmemory (status|notify check|context list)$/ },\n // Task start with optional --assign-me flag (Linear task ID is UUID format)\n {\n pattern: /^stackmemory task start ([a-f0-9-]{36})( --assign-me)?$/,\n },\n // Additional StackMemory commands for mobile/WhatsApp\n { pattern: /^stackmemory context show$/ },\n { pattern: /^stackmemory task list$/ },\n\n // Git commands\n { pattern: /^git (status|diff|log|branch)( --[a-z-]+)*$/ },\n { pattern: /^git add -A && git commit$/ },\n { pattern: /^gh pr create --fill$/ },\n // Git log with line limit for mobile-friendly output\n { pattern: /^git log --oneline -\\d{1,2}$/ },\n\n // WhatsApp/Mobile quick commands\n { pattern: /^status$/i },\n { pattern: /^tasks$/i },\n { pattern: /^context$/i },\n { pattern: /^help$/i },\n { pattern: /^sync$/i },\n\n // Claude Code launcher\n { pattern: /^claude-sm$/ },\n\n // Log viewing (safe read-only)\n { pattern: /^tail -\\d+ ~\\/\\.claude\\/logs\\/\\*\\.log$/ },\n\n // Custom aliases (cwm = claude worktree merge)\n { pattern: /^cwm$/ },\n\n // Simple echo/confirmation (no variables)\n {\n pattern:\n /^echo \"?(Done|OK|Confirmed|Acknowledged|Great work! Time for a coffee break\\.)\"?$/,\n },\n];\n\n/**\n * Check if an action is in the allowlist\n */\nfunction isActionAllowed(action: string): boolean {\n const trimmed = action.trim();\n return SAFE_ACTION_PATTERNS.some(({ pattern, validate }) => {\n const match = trimmed.match(pattern);\n if (!match) return false;\n if (validate && !validate(match)) return false;\n return true;\n });\n}\n\nexport interface ActionResult {\n success: boolean;\n output?: string;\n error?: string;\n}\n\nexport interface PendingAction {\n id: string;\n promptId: string;\n response: string;\n action: string;\n timestamp: string;\n status: 'pending' | 'running' | 'completed' | 'failed';\n result?: string;\n error?: string;\n}\n\nexport interface ActionQueue {\n actions: PendingAction[];\n lastChecked: string;\n}\n\nconst QUEUE_PATH = join(homedir(), '.stackmemory', 'sms-action-queue.json');\n\nconst DEFAULT_QUEUE: ActionQueue = {\n actions: [],\n lastChecked: new Date().toISOString(),\n};\n\nexport function loadActionQueue(): ActionQueue {\n try {\n if (existsSync(QUEUE_PATH)) {\n const data = JSON.parse(readFileSync(QUEUE_PATH, 'utf8'));\n return parseConfigSafe(\n ActionQueueSchema,\n data,\n DEFAULT_QUEUE,\n 'action-queue'\n );\n }\n } catch {\n // Use defaults\n }\n return { ...DEFAULT_QUEUE, lastChecked: new Date().toISOString() };\n}\n\nexport function saveActionQueue(queue: ActionQueue): void {\n try {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n writeFileSecure(QUEUE_PATH, JSON.stringify(queue, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nexport function queueAction(\n promptId: string,\n response: string,\n action: string\n): string {\n const queue = loadActionQueue();\n // Use cryptographically secure random ID\n const id = randomBytes(8).toString('hex');\n\n queue.actions.push({\n id,\n promptId,\n response,\n action,\n timestamp: new Date().toISOString(),\n status: 'pending',\n });\n\n saveActionQueue(queue);\n return id;\n}\n\n/**\n * Get Linear client if available\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 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 * Handle special actions that require API calls instead of shell commands\n */\nasync function handleSpecialAction(action: string): Promise<{\n handled: boolean;\n success?: boolean;\n output?: string;\n error?: string;\n}> {\n // Handle stackmemory task start command\n const taskStartMatch = action.match(\n /^stackmemory task start ([a-f0-9-]{36})( --assign-me)?$/\n );\n if (taskStartMatch) {\n const issueId = taskStartMatch[1];\n const client = getLinearClient();\n\n if (!client) {\n return {\n handled: true,\n success: false,\n error:\n 'Linear not configured. Set LINEAR_API_KEY or run stackmemory linear setup.',\n };\n }\n\n try {\n const result = await client.startIssue(issueId);\n if (result.success && result.issue) {\n return {\n handled: true,\n success: true,\n output: `Started: ${result.issue.identifier} - ${result.issue.title}`,\n };\n }\n return {\n handled: true,\n success: false,\n error: result.error || 'Failed to start issue',\n };\n } catch (err) {\n return {\n handled: true,\n success: false,\n error: err instanceof Error ? err.message : 'Unknown error',\n };\n }\n }\n\n return { handled: false };\n}\n\n/**\n * Execute an action safely using allowlist validation\n * This prevents command injection by only allowing pre-approved commands\n */\nexport async function executeActionSafe(\n action: string,\n _response: string\n): Promise<{ success: boolean; output?: string; error?: string }> {\n // Check if action is in allowlist\n if (!isActionAllowed(action)) {\n console.error(`[sms-action] Action not in allowlist: ${action}`);\n return {\n success: false,\n error: `Action not allowed. Only pre-approved commands can be executed via SMS.`,\n };\n }\n\n // Check for special actions that need API calls\n const specialResult = await handleSpecialAction(action);\n if (specialResult.handled) {\n return {\n success: specialResult.success || false,\n output: specialResult.output,\n error: specialResult.error,\n };\n }\n\n try {\n console.log(`[sms-action] Executing safe action: ${action}`);\n\n // Parse the action into command and args, respecting quotes\n const parts = parseCommandArgs(action);\n if (parts.length === 0) {\n return { success: false, error: 'Empty command' };\n }\n\n const cmd = parts[0];\n const args = parts.slice(1);\n\n // Use execFileSync for commands without shell interpretation\n // This prevents shell injection even if the allowlist is somehow bypassed\n const output = execFileSync(cmd, args, {\n encoding: 'utf8',\n timeout: 60000,\n stdio: ['pipe', 'pipe', 'pipe'],\n shell: false, // Explicitly disable shell\n });\n\n return { success: true, output };\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n return { success: false, error };\n }\n}\n\nexport function getPendingActions(): PendingAction[] {\n const queue = loadActionQueue();\n return queue.actions.filter((a) => a.status === 'pending');\n}\n\nexport function markActionRunning(id: string): void {\n const queue = loadActionQueue();\n const action = queue.actions.find((a) => a.id === id);\n if (action) {\n action.status = 'running';\n saveActionQueue(queue);\n }\n}\n\nexport function markActionCompleted(\n id: string,\n result?: string,\n error?: string\n): void {\n const queue = loadActionQueue();\n const action = queue.actions.find((a) => a.id === id);\n if (action) {\n action.status = error ? 'failed' : 'completed';\n action.result = result;\n action.error = error;\n saveActionQueue(queue);\n }\n}\n\nexport async function executeAction(action: PendingAction): Promise<{\n success: boolean;\n output?: string;\n error?: string;\n}> {\n markActionRunning(action.id);\n\n // Use the safe execution path to prevent command injection\n const result = await executeActionSafe(action.action, action.response);\n\n if (result.success) {\n markActionCompleted(action.id, result.output);\n } else {\n markActionCompleted(action.id, undefined, result.error);\n }\n\n return result;\n}\n\nexport async function processAllPendingActions(): Promise<{\n processed: number;\n succeeded: number;\n failed: number;\n}> {\n const pending = getPendingActions();\n let succeeded = 0;\n let failed = 0;\n\n for (const action of pending) {\n const result = await executeAction(action);\n if (result.success) {\n succeeded++;\n } else {\n failed++;\n }\n }\n\n return { processed: pending.length, succeeded, failed };\n}\n\n// Clean up old completed actions (keep last 50)\nexport function cleanupOldActions(): number {\n const queue = loadActionQueue();\n const completed = queue.actions.filter(\n (a) => a.status === 'completed' || a.status === 'failed'\n );\n\n if (completed.length > 50) {\n const toRemove = completed.slice(0, completed.length - 50);\n queue.actions = queue.actions.filter(\n (a) => !toRemove.find((r) => r.id === a.id)\n );\n saveActionQueue(queue);\n return toRemove.length;\n }\n\n return 0;\n}\n\n/**\n * Action Templates - Common actions for SMS responses\n *\n * SECURITY NOTE: These templates return command strings that must be\n * validated against SAFE_ACTION_PATTERNS before execution.\n * Templates that accept user input are removed to prevent injection.\n */\nexport const ACTION_TEMPLATES = {\n // Git/PR actions (PR numbers must be validated as integers)\n approvePR: (prNumber: string) => {\n // Validate PR number is numeric only\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr review ${prNumber} --approve`;\n },\n mergePR: (prNumber: string) => {\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr merge ${prNumber} --squash`;\n },\n viewPR: (prNumber: string) => {\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr view ${prNumber}`;\n },\n\n // Build actions (no user input)\n rebuild: () => `npm run build`,\n retest: () => `npm run test:run`,\n lint: () => `npm run lint:fix`,\n\n // Status actions (no user input)\n status: () => `stackmemory status`,\n checkNotifications: () => `stackmemory notify check`,\n\n // REMOVED for security - these templates allowed arbitrary user input:\n // - requestChanges (allowed arbitrary message)\n // - closePR (could be used maliciously)\n // - deploy/rollback (too dangerous for SMS)\n // - verifyDeployment (allowed arbitrary URL)\n // - notifySlack (allowed arbitrary message - command injection)\n // - notifyTeam (allowed arbitrary message - command injection)\n};\n\n/**\n * Create action string from template\n */\nexport function createAction(\n template: keyof typeof ACTION_TEMPLATES,\n ...args: string[]\n): string {\n const fn = ACTION_TEMPLATES[template];\n if (typeof fn === 'function') {\n return (fn as (...args: string[]) => string)(...args);\n }\n return fn;\n}\n\n/**\n * Watch for new actions and execute them\n */\nexport function startActionWatcher(intervalMs: number = 5000): NodeJS.Timeout {\n console.log(\n `[sms-action] Starting action watcher (interval: ${intervalMs}ms)`\n );\n\n return setInterval(() => {\n const pending = getPendingActions();\n if (pending.length > 0) {\n console.log(`[sms-action] Found ${pending.length} pending action(s)`);\n processAllPendingActions();\n }\n }, intervalMs);\n}\n\n/**\n * Integration with SMS webhook - queue action when response received\n */\nexport function handleSMSResponse(\n promptId: string,\n response: string,\n action?: string\n): void {\n if (action) {\n const actionId = queueAction(promptId, response, action);\n console.log(`[sms-action] Queued action ${actionId}: ${action}`);\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -42,18 +42,66 @@ const MAX_PHONE_LENGTH = 50;
42
42
  const MAX_BODY_SIZE = 50 * 1024;
43
43
  const RATE_LIMIT_WINDOW_MS = 60 * 1e3;
44
44
  const RATE_LIMIT_MAX_REQUESTS = 30;
45
- const rateLimitStore = /* @__PURE__ */ new Map();
45
+ const ACTION_TIMEOUT_MS = 6e4;
46
+ async function executeActionWithTimeout(action, response) {
47
+ return Promise.race([
48
+ executeActionSafe(action, response),
49
+ new Promise(
50
+ (_, reject) => setTimeout(
51
+ () => reject(new Error(`Action timed out after ${ACTION_TIMEOUT_MS}ms`)),
52
+ ACTION_TIMEOUT_MS
53
+ )
54
+ )
55
+ ]).catch((error) => ({
56
+ success: false,
57
+ error: error instanceof Error ? error.message : String(error)
58
+ }));
59
+ }
60
+ const RATE_LIMIT_PATH = join(homedir(), ".stackmemory", "rate-limits.json");
61
+ let rateLimitCache = {};
62
+ let rateLimitCacheDirty = false;
63
+ function loadRateLimits() {
64
+ try {
65
+ if (existsSync(RATE_LIMIT_PATH)) {
66
+ const data = JSON.parse(readFileSync(RATE_LIMIT_PATH, "utf8"));
67
+ const now = Date.now();
68
+ const cleaned = {};
69
+ for (const [ip, record] of Object.entries(data)) {
70
+ const r = record;
71
+ if (r.resetTime > now) {
72
+ cleaned[ip] = r;
73
+ }
74
+ }
75
+ return cleaned;
76
+ }
77
+ } catch {
78
+ }
79
+ return {};
80
+ }
81
+ function saveRateLimits() {
82
+ if (!rateLimitCacheDirty) return;
83
+ try {
84
+ ensureSecureDir(join(homedir(), ".stackmemory"));
85
+ writeFileSecure(RATE_LIMIT_PATH, JSON.stringify(rateLimitCache));
86
+ rateLimitCacheDirty = false;
87
+ } catch {
88
+ }
89
+ }
90
+ setInterval(saveRateLimits, 3e4);
91
+ rateLimitCache = loadRateLimits();
46
92
  function checkRateLimit(ip) {
47
93
  const now = Date.now();
48
- const record = rateLimitStore.get(ip);
94
+ const record = rateLimitCache[ip];
49
95
  if (!record || now > record.resetTime) {
50
- rateLimitStore.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });
96
+ rateLimitCache[ip] = { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS };
97
+ rateLimitCacheDirty = true;
51
98
  return true;
52
99
  }
53
100
  if (record.count >= RATE_LIMIT_MAX_REQUESTS) {
54
101
  return false;
55
102
  }
56
103
  record.count++;
104
+ rateLimitCacheDirty = true;
57
105
  return true;
58
106
  }
59
107
  function verifyTwilioSignature(url, params, signature) {
@@ -206,7 +254,7 @@ async function handleSMSWebhook(payload) {
206
254
  triggerResponseNotification(result.response || Body);
207
255
  if (result.action) {
208
256
  console.log(`[sms-webhook] Executing action: ${result.action}`);
209
- const actionResult = await executeActionSafe(
257
+ const actionResult = await executeActionWithTimeout(
210
258
  result.action,
211
259
  result.response || Body
212
260
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/hooks/sms-webhook.ts"],
4
- "sourcesContent": ["/**\n * SMS Webhook Handler for receiving Twilio responses\n * Can run as standalone server or integrate with existing Express app\n *\n * Security features:\n * - Twilio signature verification\n * - Rate limiting per IP\n * - Body size limits\n * - Content-type validation\n * - Safe action execution (no shell injection)\n */\n\nimport { createServer, IncomingMessage, ServerResponse } from 'http';\nimport { parse as parseUrl } from 'url';\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { createHmac } from 'crypto';\nimport { execFileSync } from 'child_process';\nimport {\n processIncomingResponse,\n loadSMSConfig,\n cleanupExpiredPrompts,\n sendNotification,\n} from './sms-notify.js';\nimport {\n queueAction,\n executeActionSafe,\n cleanupOldActions,\n} from './sms-action-runner.js';\nimport {\n isCommand,\n processCommand,\n sendCommandResponse,\n} from './whatsapp-commands.js';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport {\n logWebhookRequest,\n logRateLimit,\n logSignatureInvalid,\n logBodyTooLarge,\n logContentTypeInvalid,\n logActionAllowed,\n logActionBlocked,\n logCleanup,\n} from './security-logger.js';\n\n// Cleanup interval (5 minutes)\nconst CLEANUP_INTERVAL_MS = 5 * 60 * 1000;\n\n// Input validation constants\nconst MAX_SMS_BODY_LENGTH = 1000;\nconst MAX_PHONE_LENGTH = 50; // WhatsApp format: whatsapp:+12345678901\n\n// Security constants\nconst MAX_BODY_SIZE = 50 * 1024; // 50KB max body\nconst RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute\nconst RATE_LIMIT_MAX_REQUESTS = 30; // 30 requests per minute per IP\n\n// Rate limiting store (in production, use Redis)\nconst rateLimitStore = new Map<string, { count: number; resetTime: number }>();\n\nfunction checkRateLimit(ip: string): boolean {\n const now = Date.now();\n const record = rateLimitStore.get(ip);\n\n if (!record || now > record.resetTime) {\n rateLimitStore.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });\n return true;\n }\n\n if (record.count >= RATE_LIMIT_MAX_REQUESTS) {\n return false;\n }\n\n record.count++;\n return true;\n}\n\n// Twilio signature verification\nfunction verifyTwilioSignature(\n url: string,\n params: Record<string, string>,\n signature: string\n): boolean {\n const authToken = process.env['TWILIO_AUTH_TOKEN'];\n if (!authToken) {\n // Only allow bypass in explicit development mode\n const isDev =\n process.env['NODE_ENV'] === 'development' ||\n process.env['SKIP_TWILIO_VERIFICATION'] === 'true';\n\n if (isDev) {\n console.warn(\n '[sms-webhook] TWILIO_AUTH_TOKEN not set, skipping verification (dev mode)'\n );\n return true;\n }\n\n // In production, reject requests without auth token configured\n console.error(\n '[sms-webhook] TWILIO_AUTH_TOKEN not set - rejecting request in production'\n );\n return false;\n }\n\n // Build the data string (URL + sorted params)\n const sortedKeys = Object.keys(params).sort();\n let data = url;\n for (const key of sortedKeys) {\n data += key + params[key];\n }\n\n // Calculate expected signature\n const hmac = createHmac('sha1', authToken);\n hmac.update(data);\n const expectedSignature = hmac.digest('base64');\n\n return signature === expectedSignature;\n}\n\ninterface TwilioWebhookPayload {\n From: string;\n To: string;\n Body: string;\n MessageSid: string;\n}\n\nfunction parseFormData(body: string): Record<string, string> {\n const params = new URLSearchParams(body);\n const result: Record<string, string> = {};\n params.forEach((value, key) => {\n result[key] = value;\n });\n return result;\n}\n\n// Store response for Claude hook to pick up\nfunction storeLatestResponse(\n promptId: string,\n response: string,\n action?: string\n): void {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n const responsePath = join(\n homedir(),\n '.stackmemory',\n 'sms-latest-response.json'\n );\n writeFileSecure(\n responsePath,\n JSON.stringify({\n promptId,\n response,\n action,\n timestamp: new Date().toISOString(),\n })\n );\n}\n\n/**\n * Store incoming request for Claude to pick up\n * Used when a WhatsApp/SMS message arrives without a pending prompt\n */\nfunction storeIncomingRequest(from: string, message: string): void {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n const requestPath = join(\n homedir(),\n '.stackmemory',\n 'sms-incoming-request.json'\n );\n writeFileSecure(\n requestPath,\n JSON.stringify({\n from,\n message,\n timestamp: new Date().toISOString(),\n processed: false,\n })\n );\n}\n\n/**\n * Get pending incoming request (if any)\n */\nexport function getIncomingRequest(): {\n from: string;\n message: string;\n timestamp: string;\n processed: boolean;\n} | null {\n const requestPath = join(\n homedir(),\n '.stackmemory',\n 'sms-incoming-request.json'\n );\n if (!existsSync(requestPath)) {\n return null;\n }\n try {\n const data = JSON.parse(readFileSync(requestPath, 'utf-8'));\n if (data.processed) {\n return null;\n }\n return data;\n } catch {\n return null;\n }\n}\n\n/**\n * Mark incoming request as processed\n */\nexport function markRequestProcessed(): void {\n const requestPath = join(\n homedir(),\n '.stackmemory',\n 'sms-incoming-request.json'\n );\n if (!existsSync(requestPath)) {\n return;\n }\n try {\n const data = JSON.parse(readFileSync(requestPath, 'utf-8'));\n data.processed = true;\n writeFileSecure(requestPath, JSON.stringify(data));\n } catch {\n // Ignore errors\n }\n}\n\nexport async function handleSMSWebhook(payload: TwilioWebhookPayload): Promise<{\n response: string;\n action?: string;\n queued?: boolean;\n}> {\n const { From, Body } = payload;\n\n // Input length validation\n if (Body && Body.length > MAX_SMS_BODY_LENGTH) {\n console.log(`[sms-webhook] Body too long: ${Body.length} chars`);\n return { response: 'Message too long. Max 1000 characters.' };\n }\n\n if (From && From.length > MAX_PHONE_LENGTH) {\n console.log(\n `[sms-webhook] Phone number too long: ${From.length} chars (max ${MAX_PHONE_LENGTH}): ${From.substring(0, 30)}...`\n );\n return { response: 'Invalid phone number.' };\n }\n\n console.log(`[sms-webhook] Received from ${From}: ${Body}`);\n\n // Check for command prefix before prompt matching\n if (isCommand(Body)) {\n console.log(`[sms-webhook] Processing command: ${Body}`);\n const cmdResult = await processCommand(From, Body);\n\n if (cmdResult.handled) {\n // Send response back if we have one\n if (cmdResult.response) {\n // Don't await - fire and forget the response notification\n sendCommandResponse(cmdResult.response).catch(console.error);\n }\n\n return {\n response: cmdResult.response || 'Command processed',\n action: cmdResult.action,\n queued: false,\n };\n }\n // If not handled, fall through to regular prompt matching\n }\n\n const result = processIncomingResponse(From, Body);\n\n if (!result.matched) {\n if (result.prompt) {\n return {\n response: `Invalid response. Expected: ${result.prompt.options.map((o) => o.key).join(', ')}`,\n };\n }\n // No pending prompt - store as new incoming request for Claude\n storeIncomingRequest(From, Body);\n console.log(\n `[sms-webhook] Stored new request from ${From}: ${Body.substring(0, 50)}...`\n );\n return { response: 'Got it! Your request has been queued.' };\n }\n\n // Store response for Claude hook\n storeLatestResponse(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n // Trigger notification to alert user/Claude\n triggerResponseNotification(result.response || Body);\n\n // Execute action safely if present (no shell injection)\n if (result.action) {\n console.log(`[sms-webhook] Executing action: ${result.action}`);\n\n const actionResult = await executeActionSafe(\n result.action,\n result.response || Body\n );\n\n if (actionResult.success) {\n logActionAllowed('sms-webhook', result.action);\n console.log(\n `[sms-webhook] Action completed: ${(actionResult.output || '').substring(0, 200)}`\n );\n\n return {\n response: `Done! Action executed successfully.`,\n action: result.action,\n queued: false,\n };\n } else {\n logActionBlocked(\n 'sms-webhook',\n result.action,\n actionResult.error || 'unknown'\n );\n console.log(`[sms-webhook] Action failed: ${actionResult.error}`);\n\n // Queue for retry\n queueAction(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n return {\n response: `Action failed, queued for retry: ${(actionResult.error || '').substring(0, 50)}`,\n action: result.action,\n queued: true,\n };\n }\n }\n\n return {\n response: `Received: ${result.response}. Next action will be triggered.`,\n };\n}\n\n// Escape string for AppleScript (prevent injection)\nfunction escapeAppleScript(str: string): string {\n return str\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .substring(0, 200); // Limit length\n}\n\n// Trigger notification when response received\nfunction triggerResponseNotification(response: string): void {\n const safeMessage = escapeAppleScript(`SMS Response: ${response}`);\n\n // macOS notification using execFile (safer than execSync with shell)\n try {\n execFileSync(\n 'osascript',\n [\n '-e',\n `display notification \"${safeMessage}\" with title \"StackMemory\" sound name \"Glass\"`,\n ],\n { stdio: 'ignore', timeout: 5000 }\n );\n } catch {\n // Ignore if not on macOS\n }\n\n // Write signal file for other processes\n try {\n const signalPath = join(homedir(), '.stackmemory', 'sms-signal.txt');\n writeFileSecure(\n signalPath,\n JSON.stringify({\n type: 'sms_response',\n response,\n timestamp: new Date().toISOString(),\n })\n );\n } catch {\n // Ignore\n }\n\n console.log(`\\n*** SMS RESPONSE RECEIVED: \"${response}\" ***`);\n console.log(`*** Run: stackmemory notify run-actions ***\\n`);\n}\n\n// TwiML response helper\nfunction twimlResponse(message: string): string {\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Message>${escapeXml(message)}</Message>\n</Response>`;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\n// Standalone webhook server\nexport function startWebhookServer(port: number = 3456): void {\n const server = createServer(\n async (req: IncomingMessage, res: ServerResponse) => {\n const url = parseUrl(req.url || '/', true);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok' }));\n return;\n }\n\n // SMS webhook endpoint (incoming messages)\n if (\n (url.pathname === '/sms' ||\n url.pathname === '/sms/incoming' ||\n url.pathname === '/webhook') &&\n req.method === 'POST'\n ) {\n const clientIp = req.socket.remoteAddress || 'unknown';\n\n // Log webhook request\n logWebhookRequest(\n 'sms-webhook',\n req.method || 'POST',\n url.pathname || '/sms',\n clientIp\n );\n\n // Rate limiting\n if (!checkRateLimit(clientIp)) {\n logRateLimit('sms-webhook', clientIp);\n res.writeHead(429, {\n 'Content-Type': 'text/xml',\n 'Retry-After': '60',\n });\n res.end(twimlResponse('Too many requests. Please try again later.'));\n return;\n }\n\n // Content-type validation\n const contentType = req.headers['content-type'] || '';\n if (!contentType.includes('application/x-www-form-urlencoded')) {\n logContentTypeInvalid('sms-webhook', contentType, clientIp);\n res.writeHead(400, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Invalid content type'));\n return;\n }\n\n let body = '';\n let bodyTooLarge = false;\n\n req.on('data', (chunk) => {\n body += chunk;\n // Body size limit\n if (body.length > MAX_BODY_SIZE) {\n bodyTooLarge = true;\n logBodyTooLarge('sms-webhook', body.length, clientIp);\n req.destroy();\n }\n });\n\n req.on('end', async () => {\n if (bodyTooLarge) {\n res.writeHead(413, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Request too large'));\n return;\n }\n\n try {\n const payload = parseFormData(\n body\n ) as unknown as TwilioWebhookPayload;\n\n // Verify Twilio signature\n const twilioSignature = req.headers['x-twilio-signature'] as string;\n const webhookUrl = `${req.headers['x-forwarded-proto'] || 'http'}://${req.headers.host}${req.url}`;\n\n if (\n twilioSignature &&\n !verifyTwilioSignature(\n webhookUrl,\n payload as unknown as Record<string, string>,\n twilioSignature\n )\n ) {\n logSignatureInvalid('sms-webhook', clientIp);\n console.error('[sms-webhook] Invalid Twilio signature');\n res.writeHead(401, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Unauthorized'));\n return;\n }\n\n const result = await handleSMSWebhook(payload);\n\n res.writeHead(200, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse(result.response));\n } catch (err) {\n console.error('[sms-webhook] Error:', err);\n res.writeHead(500, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Error processing message'));\n }\n });\n return;\n }\n\n // Status callback endpoint (delivery status updates)\n if (url.pathname === '/sms/status' && req.method === 'POST') {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk;\n });\n\n req.on('end', () => {\n try {\n const payload = parseFormData(body);\n console.log(\n `[sms-webhook] Status update: ${payload['MessageSid']} -> ${payload['MessageStatus']}`\n );\n\n // Store status for tracking\n const statusPath = join(\n homedir(),\n '.stackmemory',\n 'sms-status.json'\n );\n const statuses: Record<string, string> = existsSync(statusPath)\n ? JSON.parse(readFileSync(statusPath, 'utf8'))\n : {};\n statuses[payload['MessageSid']] = payload['MessageStatus'];\n writeFileSecure(statusPath, JSON.stringify(statuses, null, 2));\n\n res.writeHead(200, { 'Content-Type': 'text/plain' });\n res.end('OK');\n } catch (err) {\n console.error('[sms-webhook] Status error:', err);\n res.writeHead(500);\n res.end('Error');\n }\n });\n return;\n }\n\n // Server status endpoint\n if (url.pathname === '/status') {\n const config = loadSMSConfig();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n enabled: config.enabled,\n pendingPrompts: config.pendingPrompts.length,\n })\n );\n return;\n }\n\n // Get pending incoming request endpoint\n if (url.pathname === '/request' && req.method === 'GET') {\n const request = getIncomingRequest();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ request }));\n return;\n }\n\n // Mark request as processed endpoint\n if (url.pathname === '/request/ack' && req.method === 'POST') {\n markRequestProcessed();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ success: true }));\n return;\n }\n\n // Send outgoing notification endpoint\n if (url.pathname === '/send' && req.method === 'POST') {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk;\n if (body.length > MAX_BODY_SIZE) {\n req.destroy();\n }\n });\n\n req.on('end', async () => {\n try {\n const payload = JSON.parse(body);\n const message = payload.message || payload.body || '';\n const title = payload.title || 'Notification';\n const type = payload.type || 'custom';\n\n if (!message) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({ success: false, error: 'Message required' })\n );\n return;\n }\n\n const result = await sendNotification({\n type: type as\n | 'task_complete'\n | 'review_ready'\n | 'error'\n | 'custom',\n title,\n message,\n });\n\n res.writeHead(result.success ? 200 : 500, {\n 'Content-Type': 'application/json',\n });\n res.end(JSON.stringify(result));\n } catch (err) {\n console.error('[sms-webhook] Send error:', err);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n success: false,\n error: err instanceof Error ? err.message : 'Send failed',\n })\n );\n }\n });\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n }\n );\n\n server.listen(port, () => {\n console.log(`[sms-webhook] Server listening on port ${port}`);\n console.log(\n `[sms-webhook] Incoming messages: http://localhost:${port}/sms/incoming`\n );\n console.log(\n `[sms-webhook] Status callback: http://localhost:${port}/sms/status`\n );\n console.log(`[sms-webhook] Configure these URLs in Twilio console`);\n\n // Start timed cleanup of expired prompts and old actions\n setInterval(() => {\n try {\n const expiredPrompts = cleanupExpiredPrompts();\n const oldActions = cleanupOldActions();\n if (expiredPrompts > 0 || oldActions > 0) {\n logCleanup('sms-webhook', expiredPrompts, oldActions);\n console.log(\n `[sms-webhook] Cleanup: ${expiredPrompts} expired prompts, ${oldActions} old actions`\n );\n }\n } catch {\n // Ignore cleanup errors\n }\n }, CLEANUP_INTERVAL_MS);\n console.log(\n `[sms-webhook] Cleanup interval: every ${CLEANUP_INTERVAL_MS / 1000}s`\n );\n });\n}\n\n// Express middleware for integration\nexport async function smsWebhookMiddleware(\n req: { body: TwilioWebhookPayload },\n res: { type: (t: string) => void; send: (s: string) => void }\n): Promise<void> {\n const result = await handleSMSWebhook(req.body);\n res.type('text/xml');\n res.send(twimlResponse(result.response));\n}\n\n// CLI entry\nif (process.argv[1]?.endsWith('sms-webhook.js')) {\n const port = parseInt(process.env['SMS_WEBHOOK_PORT'] || '3456', 10);\n startWebhookServer(port);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * SMS Webhook Handler for receiving Twilio responses\n * Can run as standalone server or integrate with existing Express app\n *\n * Security features:\n * - Twilio signature verification\n * - Rate limiting per IP\n * - Body size limits\n * - Content-type validation\n * - Safe action execution (no shell injection)\n */\n\nimport { createServer, IncomingMessage, ServerResponse } from 'http';\nimport { parse as parseUrl } from 'url';\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { createHmac } from 'crypto';\nimport { execFileSync } from 'child_process';\nimport {\n processIncomingResponse,\n loadSMSConfig,\n cleanupExpiredPrompts,\n sendNotification,\n} from './sms-notify.js';\nimport {\n queueAction,\n executeActionSafe,\n cleanupOldActions,\n type ActionResult,\n} from './sms-action-runner.js';\nimport {\n isCommand,\n processCommand,\n sendCommandResponse,\n} from './whatsapp-commands.js';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport {\n logWebhookRequest,\n logRateLimit,\n logSignatureInvalid,\n logBodyTooLarge,\n logContentTypeInvalid,\n logActionAllowed,\n logActionBlocked,\n logCleanup,\n} from './security-logger.js';\n\n// Cleanup interval (5 minutes)\nconst CLEANUP_INTERVAL_MS = 5 * 60 * 1000;\n\n// Input validation constants\nconst MAX_SMS_BODY_LENGTH = 1000;\nconst MAX_PHONE_LENGTH = 50; // WhatsApp format: whatsapp:+12345678901\n\n// Security constants\nconst MAX_BODY_SIZE = 50 * 1024; // 50KB max body\nconst RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute\nconst RATE_LIMIT_MAX_REQUESTS = 30; // 30 requests per minute per IP\nconst ACTION_TIMEOUT_MS = 60000; // 60 second timeout for action execution\n\n/**\n * Execute action with timeout to prevent hanging requests\n */\nasync function executeActionWithTimeout(\n action: string,\n response: string\n): Promise<ActionResult> {\n return Promise.race([\n executeActionSafe(action, response),\n new Promise<ActionResult>((_, reject) =>\n setTimeout(\n () =>\n reject(new Error(`Action timed out after ${ACTION_TIMEOUT_MS}ms`)),\n ACTION_TIMEOUT_MS\n )\n ),\n ]).catch((error) => ({\n success: false,\n error: error instanceof Error ? error.message : String(error),\n }));\n}\n\n// Rate limiting store - persisted to disk to survive restarts\nconst RATE_LIMIT_PATH = join(homedir(), '.stackmemory', 'rate-limits.json');\n\ninterface RateLimitRecord {\n count: number;\n resetTime: number;\n}\n\ninterface RateLimitStore {\n [ip: string]: RateLimitRecord;\n}\n\n// In-memory cache with periodic persistence\nlet rateLimitCache: RateLimitStore = {};\nlet rateLimitCacheDirty = false;\n\nfunction loadRateLimits(): RateLimitStore {\n try {\n if (existsSync(RATE_LIMIT_PATH)) {\n const data = JSON.parse(readFileSync(RATE_LIMIT_PATH, 'utf8'));\n // Clean up expired entries on load\n const now = Date.now();\n const cleaned: RateLimitStore = {};\n for (const [ip, record] of Object.entries(data)) {\n const r = record as RateLimitRecord;\n if (r.resetTime > now) {\n cleaned[ip] = r;\n }\n }\n return cleaned;\n }\n } catch {\n // Use empty store on error\n }\n return {};\n}\n\nfunction saveRateLimits(): void {\n if (!rateLimitCacheDirty) return;\n try {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n writeFileSecure(RATE_LIMIT_PATH, JSON.stringify(rateLimitCache));\n rateLimitCacheDirty = false;\n } catch {\n // Ignore save errors - rate limiting is best-effort\n }\n}\n\n// Persist rate limits periodically (every 30 seconds)\nsetInterval(saveRateLimits, 30000);\n\n// Load on startup\nrateLimitCache = loadRateLimits();\n\nfunction checkRateLimit(ip: string): boolean {\n const now = Date.now();\n const record = rateLimitCache[ip];\n\n if (!record || now > record.resetTime) {\n rateLimitCache[ip] = { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS };\n rateLimitCacheDirty = true;\n return true;\n }\n\n if (record.count >= RATE_LIMIT_MAX_REQUESTS) {\n return false;\n }\n\n record.count++;\n rateLimitCacheDirty = true;\n return true;\n}\n\n// Twilio signature verification\nfunction verifyTwilioSignature(\n url: string,\n params: Record<string, string>,\n signature: string\n): boolean {\n const authToken = process.env['TWILIO_AUTH_TOKEN'];\n if (!authToken) {\n // Only allow bypass in explicit development mode\n const isDev =\n process.env['NODE_ENV'] === 'development' ||\n process.env['SKIP_TWILIO_VERIFICATION'] === 'true';\n\n if (isDev) {\n console.warn(\n '[sms-webhook] TWILIO_AUTH_TOKEN not set, skipping verification (dev mode)'\n );\n return true;\n }\n\n // In production, reject requests without auth token configured\n console.error(\n '[sms-webhook] TWILIO_AUTH_TOKEN not set - rejecting request in production'\n );\n return false;\n }\n\n // Build the data string (URL + sorted params)\n const sortedKeys = Object.keys(params).sort();\n let data = url;\n for (const key of sortedKeys) {\n data += key + params[key];\n }\n\n // Calculate expected signature\n const hmac = createHmac('sha1', authToken);\n hmac.update(data);\n const expectedSignature = hmac.digest('base64');\n\n return signature === expectedSignature;\n}\n\ninterface TwilioWebhookPayload {\n From: string;\n To: string;\n Body: string;\n MessageSid: string;\n}\n\nfunction parseFormData(body: string): Record<string, string> {\n const params = new URLSearchParams(body);\n const result: Record<string, string> = {};\n params.forEach((value, key) => {\n result[key] = value;\n });\n return result;\n}\n\n// Store response for Claude hook to pick up\nfunction storeLatestResponse(\n promptId: string,\n response: string,\n action?: string\n): void {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n const responsePath = join(\n homedir(),\n '.stackmemory',\n 'sms-latest-response.json'\n );\n writeFileSecure(\n responsePath,\n JSON.stringify({\n promptId,\n response,\n action,\n timestamp: new Date().toISOString(),\n })\n );\n}\n\n/**\n * Store incoming request for Claude to pick up\n * Used when a WhatsApp/SMS message arrives without a pending prompt\n */\nfunction storeIncomingRequest(from: string, message: string): void {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n const requestPath = join(\n homedir(),\n '.stackmemory',\n 'sms-incoming-request.json'\n );\n writeFileSecure(\n requestPath,\n JSON.stringify({\n from,\n message,\n timestamp: new Date().toISOString(),\n processed: false,\n })\n );\n}\n\n/**\n * Get pending incoming request (if any)\n */\nexport function getIncomingRequest(): {\n from: string;\n message: string;\n timestamp: string;\n processed: boolean;\n} | null {\n const requestPath = join(\n homedir(),\n '.stackmemory',\n 'sms-incoming-request.json'\n );\n if (!existsSync(requestPath)) {\n return null;\n }\n try {\n const data = JSON.parse(readFileSync(requestPath, 'utf-8'));\n if (data.processed) {\n return null;\n }\n return data;\n } catch {\n return null;\n }\n}\n\n/**\n * Mark incoming request as processed\n */\nexport function markRequestProcessed(): void {\n const requestPath = join(\n homedir(),\n '.stackmemory',\n 'sms-incoming-request.json'\n );\n if (!existsSync(requestPath)) {\n return;\n }\n try {\n const data = JSON.parse(readFileSync(requestPath, 'utf-8'));\n data.processed = true;\n writeFileSecure(requestPath, JSON.stringify(data));\n } catch {\n // Ignore errors\n }\n}\n\nexport async function handleSMSWebhook(payload: TwilioWebhookPayload): Promise<{\n response: string;\n action?: string;\n queued?: boolean;\n}> {\n const { From, Body } = payload;\n\n // Input length validation\n if (Body && Body.length > MAX_SMS_BODY_LENGTH) {\n console.log(`[sms-webhook] Body too long: ${Body.length} chars`);\n return { response: 'Message too long. Max 1000 characters.' };\n }\n\n if (From && From.length > MAX_PHONE_LENGTH) {\n console.log(\n `[sms-webhook] Phone number too long: ${From.length} chars (max ${MAX_PHONE_LENGTH}): ${From.substring(0, 30)}...`\n );\n return { response: 'Invalid phone number.' };\n }\n\n console.log(`[sms-webhook] Received from ${From}: ${Body}`);\n\n // Check for command prefix before prompt matching\n if (isCommand(Body)) {\n console.log(`[sms-webhook] Processing command: ${Body}`);\n const cmdResult = await processCommand(From, Body);\n\n if (cmdResult.handled) {\n // Send response back if we have one\n if (cmdResult.response) {\n // Don't await - fire and forget the response notification\n sendCommandResponse(cmdResult.response).catch(console.error);\n }\n\n return {\n response: cmdResult.response || 'Command processed',\n action: cmdResult.action,\n queued: false,\n };\n }\n // If not handled, fall through to regular prompt matching\n }\n\n const result = processIncomingResponse(From, Body);\n\n if (!result.matched) {\n if (result.prompt) {\n return {\n response: `Invalid response. Expected: ${result.prompt.options.map((o) => o.key).join(', ')}`,\n };\n }\n // No pending prompt - store as new incoming request for Claude\n storeIncomingRequest(From, Body);\n console.log(\n `[sms-webhook] Stored new request from ${From}: ${Body.substring(0, 50)}...`\n );\n return { response: 'Got it! Your request has been queued.' };\n }\n\n // Store response for Claude hook\n storeLatestResponse(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n // Trigger notification to alert user/Claude\n triggerResponseNotification(result.response || Body);\n\n // Execute action safely if present (no shell injection, with timeout)\n if (result.action) {\n console.log(`[sms-webhook] Executing action: ${result.action}`);\n\n const actionResult = await executeActionWithTimeout(\n result.action,\n result.response || Body\n );\n\n if (actionResult.success) {\n logActionAllowed('sms-webhook', result.action);\n console.log(\n `[sms-webhook] Action completed: ${(actionResult.output || '').substring(0, 200)}`\n );\n\n return {\n response: `Done! Action executed successfully.`,\n action: result.action,\n queued: false,\n };\n } else {\n logActionBlocked(\n 'sms-webhook',\n result.action,\n actionResult.error || 'unknown'\n );\n console.log(`[sms-webhook] Action failed: ${actionResult.error}`);\n\n // Queue for retry\n queueAction(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n return {\n response: `Action failed, queued for retry: ${(actionResult.error || '').substring(0, 50)}`,\n action: result.action,\n queued: true,\n };\n }\n }\n\n return {\n response: `Received: ${result.response}. Next action will be triggered.`,\n };\n}\n\n// Escape string for AppleScript (prevent injection)\nfunction escapeAppleScript(str: string): string {\n return str\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .substring(0, 200); // Limit length\n}\n\n// Trigger notification when response received\nfunction triggerResponseNotification(response: string): void {\n const safeMessage = escapeAppleScript(`SMS Response: ${response}`);\n\n // macOS notification using execFile (safer than execSync with shell)\n try {\n execFileSync(\n 'osascript',\n [\n '-e',\n `display notification \"${safeMessage}\" with title \"StackMemory\" sound name \"Glass\"`,\n ],\n { stdio: 'ignore', timeout: 5000 }\n );\n } catch {\n // Ignore if not on macOS\n }\n\n // Write signal file for other processes\n try {\n const signalPath = join(homedir(), '.stackmemory', 'sms-signal.txt');\n writeFileSecure(\n signalPath,\n JSON.stringify({\n type: 'sms_response',\n response,\n timestamp: new Date().toISOString(),\n })\n );\n } catch {\n // Ignore\n }\n\n console.log(`\\n*** SMS RESPONSE RECEIVED: \"${response}\" ***`);\n console.log(`*** Run: stackmemory notify run-actions ***\\n`);\n}\n\n// TwiML response helper\nfunction twimlResponse(message: string): string {\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Message>${escapeXml(message)}</Message>\n</Response>`;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\n// Standalone webhook server\nexport function startWebhookServer(port: number = 3456): void {\n const server = createServer(\n async (req: IncomingMessage, res: ServerResponse) => {\n const url = parseUrl(req.url || '/', true);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok' }));\n return;\n }\n\n // SMS webhook endpoint (incoming messages)\n if (\n (url.pathname === '/sms' ||\n url.pathname === '/sms/incoming' ||\n url.pathname === '/webhook') &&\n req.method === 'POST'\n ) {\n const clientIp = req.socket.remoteAddress || 'unknown';\n\n // Log webhook request\n logWebhookRequest(\n 'sms-webhook',\n req.method || 'POST',\n url.pathname || '/sms',\n clientIp\n );\n\n // Rate limiting\n if (!checkRateLimit(clientIp)) {\n logRateLimit('sms-webhook', clientIp);\n res.writeHead(429, {\n 'Content-Type': 'text/xml',\n 'Retry-After': '60',\n });\n res.end(twimlResponse('Too many requests. Please try again later.'));\n return;\n }\n\n // Content-type validation\n const contentType = req.headers['content-type'] || '';\n if (!contentType.includes('application/x-www-form-urlencoded')) {\n logContentTypeInvalid('sms-webhook', contentType, clientIp);\n res.writeHead(400, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Invalid content type'));\n return;\n }\n\n let body = '';\n let bodyTooLarge = false;\n\n req.on('data', (chunk) => {\n body += chunk;\n // Body size limit\n if (body.length > MAX_BODY_SIZE) {\n bodyTooLarge = true;\n logBodyTooLarge('sms-webhook', body.length, clientIp);\n req.destroy();\n }\n });\n\n req.on('end', async () => {\n if (bodyTooLarge) {\n res.writeHead(413, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Request too large'));\n return;\n }\n\n try {\n const payload = parseFormData(\n body\n ) as unknown as TwilioWebhookPayload;\n\n // Verify Twilio signature\n const twilioSignature = req.headers['x-twilio-signature'] as string;\n const webhookUrl = `${req.headers['x-forwarded-proto'] || 'http'}://${req.headers.host}${req.url}`;\n\n if (\n twilioSignature &&\n !verifyTwilioSignature(\n webhookUrl,\n payload as unknown as Record<string, string>,\n twilioSignature\n )\n ) {\n logSignatureInvalid('sms-webhook', clientIp);\n console.error('[sms-webhook] Invalid Twilio signature');\n res.writeHead(401, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Unauthorized'));\n return;\n }\n\n const result = await handleSMSWebhook(payload);\n\n res.writeHead(200, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse(result.response));\n } catch (err) {\n console.error('[sms-webhook] Error:', err);\n res.writeHead(500, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Error processing message'));\n }\n });\n return;\n }\n\n // Status callback endpoint (delivery status updates)\n if (url.pathname === '/sms/status' && req.method === 'POST') {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk;\n });\n\n req.on('end', () => {\n try {\n const payload = parseFormData(body);\n console.log(\n `[sms-webhook] Status update: ${payload['MessageSid']} -> ${payload['MessageStatus']}`\n );\n\n // Store status for tracking\n const statusPath = join(\n homedir(),\n '.stackmemory',\n 'sms-status.json'\n );\n const statuses: Record<string, string> = existsSync(statusPath)\n ? JSON.parse(readFileSync(statusPath, 'utf8'))\n : {};\n statuses[payload['MessageSid']] = payload['MessageStatus'];\n writeFileSecure(statusPath, JSON.stringify(statuses, null, 2));\n\n res.writeHead(200, { 'Content-Type': 'text/plain' });\n res.end('OK');\n } catch (err) {\n console.error('[sms-webhook] Status error:', err);\n res.writeHead(500);\n res.end('Error');\n }\n });\n return;\n }\n\n // Server status endpoint\n if (url.pathname === '/status') {\n const config = loadSMSConfig();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n enabled: config.enabled,\n pendingPrompts: config.pendingPrompts.length,\n })\n );\n return;\n }\n\n // Get pending incoming request endpoint\n if (url.pathname === '/request' && req.method === 'GET') {\n const request = getIncomingRequest();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ request }));\n return;\n }\n\n // Mark request as processed endpoint\n if (url.pathname === '/request/ack' && req.method === 'POST') {\n markRequestProcessed();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ success: true }));\n return;\n }\n\n // Send outgoing notification endpoint\n if (url.pathname === '/send' && req.method === 'POST') {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk;\n if (body.length > MAX_BODY_SIZE) {\n req.destroy();\n }\n });\n\n req.on('end', async () => {\n try {\n const payload = JSON.parse(body);\n const message = payload.message || payload.body || '';\n const title = payload.title || 'Notification';\n const type = payload.type || 'custom';\n\n if (!message) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({ success: false, error: 'Message required' })\n );\n return;\n }\n\n const result = await sendNotification({\n type: type as\n | 'task_complete'\n | 'review_ready'\n | 'error'\n | 'custom',\n title,\n message,\n });\n\n res.writeHead(result.success ? 200 : 500, {\n 'Content-Type': 'application/json',\n });\n res.end(JSON.stringify(result));\n } catch (err) {\n console.error('[sms-webhook] Send error:', err);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n success: false,\n error: err instanceof Error ? err.message : 'Send failed',\n })\n );\n }\n });\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n }\n );\n\n server.listen(port, () => {\n console.log(`[sms-webhook] Server listening on port ${port}`);\n console.log(\n `[sms-webhook] Incoming messages: http://localhost:${port}/sms/incoming`\n );\n console.log(\n `[sms-webhook] Status callback: http://localhost:${port}/sms/status`\n );\n console.log(`[sms-webhook] Configure these URLs in Twilio console`);\n\n // Start timed cleanup of expired prompts and old actions\n setInterval(() => {\n try {\n const expiredPrompts = cleanupExpiredPrompts();\n const oldActions = cleanupOldActions();\n if (expiredPrompts > 0 || oldActions > 0) {\n logCleanup('sms-webhook', expiredPrompts, oldActions);\n console.log(\n `[sms-webhook] Cleanup: ${expiredPrompts} expired prompts, ${oldActions} old actions`\n );\n }\n } catch {\n // Ignore cleanup errors\n }\n }, CLEANUP_INTERVAL_MS);\n console.log(\n `[sms-webhook] Cleanup interval: every ${CLEANUP_INTERVAL_MS / 1000}s`\n );\n });\n}\n\n// Express middleware for integration\nexport async function smsWebhookMiddleware(\n req: { body: TwilioWebhookPayload },\n res: { type: (t: string) => void; send: (s: string) => void }\n): Promise<void> {\n const result = await handleSMSWebhook(req.body);\n res.type('text/xml');\n res.send(twimlResponse(result.response));\n}\n\n// CLI entry\nif (process.argv[1]?.endsWith('sms-webhook.js')) {\n const port = parseInt(process.env['SMS_WEBHOOK_PORT'] || '3456', 10);\n startWebhookServer(port);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -5,21 +5,19 @@ const __dirname = __pathDirname(__filename);
5
5
  import { EventEmitter } from "events";
6
6
  import { logger } from "../../core/monitoring/logger.js";
7
7
  import { LinearSyncEngine } from "./sync.js";
8
+ import { AsyncMutex } from "../../core/utils/async-mutex.js";
8
9
  class LinearSyncManager extends EventEmitter {
9
10
  syncEngine;
10
11
  syncTimer;
11
12
  pendingSyncTimer;
12
13
  config;
13
14
  lastSyncTime = 0;
14
- syncInProgress = false;
15
- syncLockAcquired = 0;
16
- // Timestamp when lock was acquired
17
- SYNC_LOCK_TIMEOUT = 3e5;
18
- // 5 minutes max sync time
15
+ syncMutex;
19
16
  taskStore;
20
17
  constructor(taskStore, authManager, config, projectRoot) {
21
18
  super();
22
19
  this.taskStore = taskStore;
20
+ this.syncMutex = new AsyncMutex(3e5);
23
21
  this.config = {
24
22
  ...config,
25
23
  autoSyncInterval: config.autoSyncInterval || 15,
@@ -88,6 +86,7 @@ class LinearSyncManager extends EventEmitter {
88
86
  }
89
87
  /**
90
88
  * Perform a sync operation
89
+ * Uses mutex to prevent concurrent sync operations (thread-safe)
91
90
  */
92
91
  async performSync(trigger) {
93
92
  if (!this.config.enabled) {
@@ -98,7 +97,8 @@ class LinearSyncManager extends EventEmitter {
98
97
  errors: ["Sync is disabled"]
99
98
  };
100
99
  }
101
- if (this.syncInProgress) {
100
+ const release = this.syncMutex.tryAcquire(`linear-sync-${trigger}`);
101
+ if (!release) {
102
102
  logger.warn(`Linear sync already in progress, skipping ${trigger} sync`);
103
103
  return {
104
104
  success: false,
@@ -107,24 +107,23 @@ class LinearSyncManager extends EventEmitter {
107
107
  errors: ["Sync already in progress"]
108
108
  };
109
109
  }
110
- const now = Date.now();
111
- const timeSinceLastSync = now - this.lastSyncTime;
112
- const minInterval = 1e4;
113
- if (trigger !== "manual" && timeSinceLastSync < minInterval) {
114
- logger.debug(
115
- `Skipping ${trigger} sync, too soon since last sync (${timeSinceLastSync}ms ago)`
116
- );
117
- return {
118
- success: false,
119
- synced: { toLinear: 0, fromLinear: 0, updated: 0 },
120
- conflicts: [],
121
- errors: [
122
- `Too soon since last sync (wait ${minInterval - timeSinceLastSync}ms)`
123
- ]
124
- };
125
- }
126
110
  try {
127
- this.syncInProgress = true;
111
+ const now = Date.now();
112
+ const timeSinceLastSync = now - this.lastSyncTime;
113
+ const minInterval = 1e4;
114
+ if (trigger !== "manual" && timeSinceLastSync < minInterval) {
115
+ logger.debug(
116
+ `Skipping ${trigger} sync, too soon since last sync (${timeSinceLastSync}ms ago)`
117
+ );
118
+ return {
119
+ success: false,
120
+ synced: { toLinear: 0, fromLinear: 0, updated: 0 },
121
+ conflicts: [],
122
+ errors: [
123
+ `Too soon since last sync (wait ${minInterval - timeSinceLastSync}ms)`
124
+ ]
125
+ };
126
+ }
128
127
  this.emit("sync:started", { trigger });
129
128
  logger.info(`Starting Linear sync (trigger: ${trigger})`);
130
129
  const result = await this.syncEngine.sync();
@@ -151,7 +150,7 @@ class LinearSyncManager extends EventEmitter {
151
150
  this.emit("sync:failed", { trigger, result, error });
152
151
  return result;
153
152
  } finally {
154
- this.syncInProgress = false;
153
+ release();
155
154
  }
156
155
  }
157
156
  /**
@@ -189,7 +188,7 @@ class LinearSyncManager extends EventEmitter {
189
188
  const nextSyncTime = this.config.autoSync && this.config.autoSyncInterval ? this.lastSyncTime + this.config.autoSyncInterval * 60 * 1e3 : null;
190
189
  return {
191
190
  enabled: this.config.enabled,
192
- syncInProgress: this.syncInProgress,
191
+ syncInProgress: this.syncMutex.isLocked(),
193
192
  lastSyncTime: this.lastSyncTime,
194
193
  nextSyncTime,
195
194
  config: this.config
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/integrations/linear/sync-manager.ts"],
4
- "sourcesContent": ["/**\n * Linear Sync Manager\n * Handles periodic and event-based synchronization with Linear\n */\n\nimport { EventEmitter } from 'events';\nimport { logger } from '../../core/monitoring/logger.js';\nimport { LinearSyncEngine, SyncConfig, SyncResult } from './sync.js';\nimport { LinearTaskManager } from '../../features/tasks/linear-task-manager.js';\nimport { LinearAuthManager } from './auth.js';\n\nexport interface SyncManagerConfig extends SyncConfig {\n autoSyncInterval?: number; // minutes\n syncOnTaskChange?: boolean;\n syncOnSessionStart?: boolean;\n syncOnSessionEnd?: boolean;\n debounceInterval?: number; // milliseconds\n}\n\nexport class LinearSyncManager extends EventEmitter {\n private syncEngine: LinearSyncEngine;\n private syncTimer?: NodeJS.Timeout;\n private pendingSyncTimer?: NodeJS.Timeout;\n private config: SyncManagerConfig;\n private lastSyncTime: number = 0;\n private syncInProgress: boolean = false;\n private syncLockAcquired: number = 0; // Timestamp when lock was acquired\n private readonly SYNC_LOCK_TIMEOUT = 300000; // 5 minutes max sync time\n private taskStore: LinearTaskManager;\n\n constructor(\n taskStore: LinearTaskManager,\n authManager: LinearAuthManager,\n config: SyncManagerConfig,\n projectRoot?: string\n ) {\n super();\n this.taskStore = taskStore;\n this.config = {\n ...config,\n autoSyncInterval: config.autoSyncInterval || 15,\n syncOnTaskChange: config.syncOnTaskChange !== false,\n syncOnSessionStart: config.syncOnSessionStart !== false,\n syncOnSessionEnd: config.syncOnSessionEnd !== false,\n debounceInterval: config.debounceInterval || 5000, // 5 seconds\n };\n\n this.syncEngine = new LinearSyncEngine(\n taskStore,\n authManager,\n config,\n projectRoot\n );\n\n this.setupEventListeners();\n this.setupPeriodicSync();\n }\n\n /**\n * Setup event listeners for automatic sync triggers\n */\n private setupEventListeners(): void {\n if (this.config.syncOnTaskChange && this.taskStore) {\n // Listen for task changes to trigger sync\n this.taskStore.on('sync:needed', (changeType: string) => {\n logger.debug(`Task change detected: ${changeType}`);\n this.scheduleDebouncedSync();\n });\n\n // Listen for specific task events if needed for logging\n this.taskStore.on('task:created', (task: any) => {\n logger.debug(`Task created: ${task.title}`);\n });\n\n this.taskStore.on('task:completed', (task: any) => {\n logger.debug(`Task completed: ${task.title}`);\n });\n\n logger.info('Task change sync enabled via EventEmitter');\n }\n }\n\n /**\n * Setup periodic sync timer\n */\n private setupPeriodicSync(): void {\n if (!this.config.autoSync || !this.config.autoSyncInterval) {\n return;\n }\n\n // Clear existing timer if any\n if (this.syncTimer) {\n clearInterval(this.syncTimer);\n }\n\n // Setup new timer\n const intervalMs = this.config.autoSyncInterval * 60 * 1000;\n this.syncTimer = setInterval(() => {\n this.performSync('periodic');\n }, intervalMs);\n\n logger.info(\n `Periodic Linear sync enabled: every ${this.config.autoSyncInterval} minutes`\n );\n }\n\n /**\n * Schedule a debounced sync to avoid too frequent syncs\n */\n private scheduleDebouncedSync(): void {\n if (!this.config.enabled) return;\n\n // Clear existing pending sync\n if (this.pendingSyncTimer) {\n clearTimeout(this.pendingSyncTimer);\n }\n\n // Schedule new sync\n this.pendingSyncTimer = setTimeout(() => {\n this.performSync('task-change');\n }, this.config.debounceInterval);\n }\n\n /**\n * Perform a sync operation\n */\n async performSync(\n trigger:\n | 'manual'\n | 'periodic'\n | 'task-change'\n | 'session-start'\n | 'session-end'\n ): Promise<SyncResult> {\n if (!this.config.enabled) {\n return {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: ['Sync is disabled'],\n };\n }\n\n if (this.syncInProgress) {\n logger.warn(`Linear sync already in progress, skipping ${trigger} sync`);\n return {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: ['Sync already in progress'],\n };\n }\n\n // Check minimum time between syncs (avoid rapid fire)\n const now = Date.now();\n const timeSinceLastSync = now - this.lastSyncTime;\n const minInterval = 10000; // 10 seconds minimum between syncs\n\n if (trigger !== 'manual' && timeSinceLastSync < minInterval) {\n logger.debug(\n `Skipping ${trigger} sync, too soon since last sync (${timeSinceLastSync}ms ago)`\n );\n return {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: [\n `Too soon since last sync (wait ${minInterval - timeSinceLastSync}ms)`,\n ],\n };\n }\n\n try {\n this.syncInProgress = true;\n this.emit('sync:started', { trigger });\n\n logger.info(`Starting Linear sync (trigger: ${trigger})`);\n const result = await this.syncEngine.sync();\n\n this.lastSyncTime = now;\n\n if (result.success) {\n logger.info(\n `Linear sync completed: ${result.synced.toLinear} to Linear, ${result.synced.fromLinear} from Linear, ${result.synced.updated} updated`\n );\n this.emit('sync:completed', { trigger, result });\n } else {\n logger.error(`Linear sync failed: ${result.errors.join(', ')}`);\n this.emit('sync:failed', { trigger, result });\n }\n\n return result;\n } catch (error: unknown) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n logger.error(`Linear sync error: ${errorMessage}`);\n\n const result: SyncResult = {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: [errorMessage],\n };\n\n this.emit('sync:failed', { trigger, result, error });\n return result;\n } finally {\n this.syncInProgress = false;\n }\n }\n\n /**\n * Sync on session start\n */\n async syncOnStart(): Promise<SyncResult | null> {\n if (this.config.syncOnSessionStart) {\n return await this.performSync('session-start');\n }\n return null;\n }\n\n /**\n * Sync on session end\n */\n async syncOnEnd(): Promise<SyncResult | null> {\n if (this.config.syncOnSessionEnd) {\n return await this.performSync('session-end');\n }\n return null;\n }\n\n /**\n * Update sync configuration\n */\n updateConfig(newConfig: Partial<SyncManagerConfig>): void {\n this.config = { ...this.config, ...newConfig };\n this.syncEngine.updateConfig(newConfig);\n\n // Restart periodic sync if interval changed\n if (\n newConfig.autoSyncInterval !== undefined ||\n newConfig.autoSync !== undefined\n ) {\n this.setupPeriodicSync();\n }\n }\n\n /**\n * Get sync status\n */\n getStatus(): {\n enabled: boolean;\n syncInProgress: boolean;\n lastSyncTime: number;\n nextSyncTime: number | null;\n config: SyncManagerConfig;\n } {\n const nextSyncTime =\n this.config.autoSync && this.config.autoSyncInterval\n ? this.lastSyncTime + this.config.autoSyncInterval * 60 * 1000\n : null;\n\n return {\n enabled: this.config.enabled,\n syncInProgress: this.syncInProgress,\n lastSyncTime: this.lastSyncTime,\n nextSyncTime,\n config: this.config,\n };\n }\n\n /**\n * Stop all sync activities\n */\n stop(): void {\n if (this.syncTimer) {\n clearInterval(this.syncTimer);\n this.syncTimer = undefined;\n }\n\n if (this.pendingSyncTimer) {\n clearTimeout(this.pendingSyncTimer);\n this.pendingSyncTimer = undefined;\n }\n\n this.removeAllListeners();\n logger.info('Linear sync manager stopped');\n }\n\n /**\n * Force an immediate sync\n */\n async forceSync(): Promise<SyncResult> {\n return await this.performSync('manual');\n }\n}\n\n/**\n * Default sync manager configuration\n */\nexport const DEFAULT_SYNC_MANAGER_CONFIG: SyncManagerConfig = {\n enabled: true,\n direction: 'bidirectional',\n autoSync: true,\n autoSyncInterval: 15, // minutes\n conflictResolution: 'newest_wins',\n syncOnTaskChange: true,\n syncOnSessionStart: true,\n syncOnSessionEnd: true,\n debounceInterval: 5000, // 5 seconds\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Linear Sync Manager\n * Handles periodic and event-based synchronization with Linear\n */\n\nimport { EventEmitter } from 'events';\nimport { logger } from '../../core/monitoring/logger.js';\nimport { LinearSyncEngine, SyncConfig, SyncResult } from './sync.js';\nimport { LinearTaskManager } from '../../features/tasks/linear-task-manager.js';\nimport { LinearAuthManager } from './auth.js';\nimport { AsyncMutex } from '../../core/utils/async-mutex.js';\n\nexport interface SyncManagerConfig extends SyncConfig {\n autoSyncInterval?: number; // minutes\n syncOnTaskChange?: boolean;\n syncOnSessionStart?: boolean;\n syncOnSessionEnd?: boolean;\n debounceInterval?: number; // milliseconds\n}\n\nexport class LinearSyncManager extends EventEmitter {\n private syncEngine: LinearSyncEngine;\n private syncTimer?: NodeJS.Timeout;\n private pendingSyncTimer?: NodeJS.Timeout;\n private config: SyncManagerConfig;\n private lastSyncTime: number = 0;\n private syncMutex: AsyncMutex;\n private taskStore: LinearTaskManager;\n\n constructor(\n taskStore: LinearTaskManager,\n authManager: LinearAuthManager,\n config: SyncManagerConfig,\n projectRoot?: string\n ) {\n super();\n this.taskStore = taskStore;\n this.syncMutex = new AsyncMutex(300000); // 5 minute lock timeout\n this.config = {\n ...config,\n autoSyncInterval: config.autoSyncInterval || 15,\n syncOnTaskChange: config.syncOnTaskChange !== false,\n syncOnSessionStart: config.syncOnSessionStart !== false,\n syncOnSessionEnd: config.syncOnSessionEnd !== false,\n debounceInterval: config.debounceInterval || 5000, // 5 seconds\n };\n\n this.syncEngine = new LinearSyncEngine(\n taskStore,\n authManager,\n config,\n projectRoot\n );\n\n this.setupEventListeners();\n this.setupPeriodicSync();\n }\n\n /**\n * Setup event listeners for automatic sync triggers\n */\n private setupEventListeners(): void {\n if (this.config.syncOnTaskChange && this.taskStore) {\n // Listen for task changes to trigger sync\n this.taskStore.on('sync:needed', (changeType: string) => {\n logger.debug(`Task change detected: ${changeType}`);\n this.scheduleDebouncedSync();\n });\n\n // Listen for specific task events if needed for logging\n this.taskStore.on('task:created', (task: any) => {\n logger.debug(`Task created: ${task.title}`);\n });\n\n this.taskStore.on('task:completed', (task: any) => {\n logger.debug(`Task completed: ${task.title}`);\n });\n\n logger.info('Task change sync enabled via EventEmitter');\n }\n }\n\n /**\n * Setup periodic sync timer\n */\n private setupPeriodicSync(): void {\n if (!this.config.autoSync || !this.config.autoSyncInterval) {\n return;\n }\n\n // Clear existing timer if any\n if (this.syncTimer) {\n clearInterval(this.syncTimer);\n }\n\n // Setup new timer\n const intervalMs = this.config.autoSyncInterval * 60 * 1000;\n this.syncTimer = setInterval(() => {\n this.performSync('periodic');\n }, intervalMs);\n\n logger.info(\n `Periodic Linear sync enabled: every ${this.config.autoSyncInterval} minutes`\n );\n }\n\n /**\n * Schedule a debounced sync to avoid too frequent syncs\n */\n private scheduleDebouncedSync(): void {\n if (!this.config.enabled) return;\n\n // Clear existing pending sync\n if (this.pendingSyncTimer) {\n clearTimeout(this.pendingSyncTimer);\n }\n\n // Schedule new sync\n this.pendingSyncTimer = setTimeout(() => {\n this.performSync('task-change');\n }, this.config.debounceInterval);\n }\n\n /**\n * Perform a sync operation\n * Uses mutex to prevent concurrent sync operations (thread-safe)\n */\n async performSync(\n trigger:\n | 'manual'\n | 'periodic'\n | 'task-change'\n | 'session-start'\n | 'session-end'\n ): Promise<SyncResult> {\n if (!this.config.enabled) {\n return {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: ['Sync is disabled'],\n };\n }\n\n // Try to acquire lock without waiting (non-blocking check)\n const release = this.syncMutex.tryAcquire(`linear-sync-${trigger}`);\n if (!release) {\n logger.warn(`Linear sync already in progress, skipping ${trigger} sync`);\n return {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: ['Sync already in progress'],\n };\n }\n\n try {\n // Check minimum time between syncs (avoid rapid fire)\n const now = Date.now();\n const timeSinceLastSync = now - this.lastSyncTime;\n const minInterval = 10000; // 10 seconds minimum between syncs\n\n if (trigger !== 'manual' && timeSinceLastSync < minInterval) {\n logger.debug(\n `Skipping ${trigger} sync, too soon since last sync (${timeSinceLastSync}ms ago)`\n );\n return {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: [\n `Too soon since last sync (wait ${minInterval - timeSinceLastSync}ms)`,\n ],\n };\n }\n\n this.emit('sync:started', { trigger });\n\n logger.info(`Starting Linear sync (trigger: ${trigger})`);\n const result = await this.syncEngine.sync();\n\n this.lastSyncTime = now;\n\n if (result.success) {\n logger.info(\n `Linear sync completed: ${result.synced.toLinear} to Linear, ${result.synced.fromLinear} from Linear, ${result.synced.updated} updated`\n );\n this.emit('sync:completed', { trigger, result });\n } else {\n logger.error(`Linear sync failed: ${result.errors.join(', ')}`);\n this.emit('sync:failed', { trigger, result });\n }\n\n return result;\n } catch (error: unknown) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n logger.error(`Linear sync error: ${errorMessage}`);\n\n const result: SyncResult = {\n success: false,\n synced: { toLinear: 0, fromLinear: 0, updated: 0 },\n conflicts: [],\n errors: [errorMessage],\n };\n\n this.emit('sync:failed', { trigger, result, error });\n return result;\n } finally {\n release(); // Always release the lock\n }\n }\n\n /**\n * Sync on session start\n */\n async syncOnStart(): Promise<SyncResult | null> {\n if (this.config.syncOnSessionStart) {\n return await this.performSync('session-start');\n }\n return null;\n }\n\n /**\n * Sync on session end\n */\n async syncOnEnd(): Promise<SyncResult | null> {\n if (this.config.syncOnSessionEnd) {\n return await this.performSync('session-end');\n }\n return null;\n }\n\n /**\n * Update sync configuration\n */\n updateConfig(newConfig: Partial<SyncManagerConfig>): void {\n this.config = { ...this.config, ...newConfig };\n this.syncEngine.updateConfig(newConfig);\n\n // Restart periodic sync if interval changed\n if (\n newConfig.autoSyncInterval !== undefined ||\n newConfig.autoSync !== undefined\n ) {\n this.setupPeriodicSync();\n }\n }\n\n /**\n * Get sync status\n */\n getStatus(): {\n enabled: boolean;\n syncInProgress: boolean;\n lastSyncTime: number;\n nextSyncTime: number | null;\n config: SyncManagerConfig;\n } {\n const nextSyncTime =\n this.config.autoSync && this.config.autoSyncInterval\n ? this.lastSyncTime + this.config.autoSyncInterval * 60 * 1000\n : null;\n\n return {\n enabled: this.config.enabled,\n syncInProgress: this.syncMutex.isLocked(),\n lastSyncTime: this.lastSyncTime,\n nextSyncTime,\n config: this.config,\n };\n }\n\n /**\n * Stop all sync activities\n */\n stop(): void {\n if (this.syncTimer) {\n clearInterval(this.syncTimer);\n this.syncTimer = undefined;\n }\n\n if (this.pendingSyncTimer) {\n clearTimeout(this.pendingSyncTimer);\n this.pendingSyncTimer = undefined;\n }\n\n this.removeAllListeners();\n logger.info('Linear sync manager stopped');\n }\n\n /**\n * Force an immediate sync\n */\n async forceSync(): Promise<SyncResult> {\n return await this.performSync('manual');\n }\n}\n\n/**\n * Default sync manager configuration\n */\nexport const DEFAULT_SYNC_MANAGER_CONFIG: SyncManagerConfig = {\n enabled: true,\n direction: 'bidirectional',\n autoSync: true,\n autoSyncInterval: 15, // minutes\n conflictResolution: 'newest_wins',\n syncOnTaskChange: true,\n syncOnSessionStart: true,\n syncOnSessionEnd: true,\n debounceInterval: 5000, // 5 seconds\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.38",
3
+ "version": "0.5.39",
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",