@stackmemoryai/stackmemory 0.5.46 → 0.5.48

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,149 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+ const DEFAULT_DAEMON_CONFIG = {
9
+ version: "1.0.0",
10
+ context: {
11
+ enabled: true,
12
+ interval: 15,
13
+ // 15 minutes
14
+ checkpointMessage: "Auto-checkpoint"
15
+ },
16
+ linear: {
17
+ enabled: false,
18
+ // Disabled by default, requires setup
19
+ interval: 60,
20
+ // 60 minutes
21
+ quietHours: { start: 22, end: 7 },
22
+ retryAttempts: 3,
23
+ retryDelay: 3e4
24
+ },
25
+ fileWatch: {
26
+ enabled: false,
27
+ // Disabled by default
28
+ interval: 0,
29
+ // Not interval-based
30
+ paths: ["."],
31
+ extensions: [".ts", ".js", ".tsx", ".jsx", ".py", ".go", ".rs"],
32
+ ignore: ["node_modules", ".git", "dist", "build", ".stackmemory"],
33
+ debounceMs: 2e3
34
+ },
35
+ heartbeatInterval: 60,
36
+ // 1 minute
37
+ inactivityTimeout: 0,
38
+ // Disabled by default
39
+ logLevel: "info"
40
+ };
41
+ function getDaemonDir() {
42
+ const dir = join(homedir(), ".stackmemory", "daemon");
43
+ if (!existsSync(dir)) {
44
+ mkdirSync(dir, { recursive: true });
45
+ }
46
+ return dir;
47
+ }
48
+ function getLogsDir() {
49
+ const dir = join(homedir(), ".stackmemory", "logs");
50
+ if (!existsSync(dir)) {
51
+ mkdirSync(dir, { recursive: true });
52
+ }
53
+ return dir;
54
+ }
55
+ function getDaemonPaths() {
56
+ const daemonDir = getDaemonDir();
57
+ const logsDir = getLogsDir();
58
+ return {
59
+ pidFile: join(daemonDir, "daemon.pid"),
60
+ statusFile: join(daemonDir, "daemon.status"),
61
+ configFile: join(daemonDir, "config.json"),
62
+ logFile: join(logsDir, "daemon.log")
63
+ };
64
+ }
65
+ function loadDaemonConfig() {
66
+ const { configFile } = getDaemonPaths();
67
+ if (!existsSync(configFile)) {
68
+ return { ...DEFAULT_DAEMON_CONFIG };
69
+ }
70
+ try {
71
+ const content = readFileSync(configFile, "utf8");
72
+ const config = JSON.parse(content);
73
+ return {
74
+ ...DEFAULT_DAEMON_CONFIG,
75
+ ...config,
76
+ context: { ...DEFAULT_DAEMON_CONFIG.context, ...config.context },
77
+ linear: { ...DEFAULT_DAEMON_CONFIG.linear, ...config.linear },
78
+ fileWatch: { ...DEFAULT_DAEMON_CONFIG.fileWatch, ...config.fileWatch }
79
+ };
80
+ } catch {
81
+ return { ...DEFAULT_DAEMON_CONFIG };
82
+ }
83
+ }
84
+ function saveDaemonConfig(config) {
85
+ const { configFile } = getDaemonPaths();
86
+ const currentConfig = loadDaemonConfig();
87
+ const newConfig = {
88
+ ...currentConfig,
89
+ ...config,
90
+ context: { ...currentConfig.context, ...config.context },
91
+ linear: { ...currentConfig.linear, ...config.linear },
92
+ fileWatch: { ...currentConfig.fileWatch, ...config.fileWatch }
93
+ };
94
+ writeFileSync(configFile, JSON.stringify(newConfig, null, 2));
95
+ }
96
+ function readDaemonStatus() {
97
+ const { statusFile, pidFile } = getDaemonPaths();
98
+ const defaultStatus = {
99
+ running: false,
100
+ services: {
101
+ context: { enabled: false },
102
+ linear: { enabled: false },
103
+ fileWatch: { enabled: false }
104
+ },
105
+ errors: []
106
+ };
107
+ if (!existsSync(pidFile)) {
108
+ return defaultStatus;
109
+ }
110
+ try {
111
+ const pidContent = readFileSync(pidFile, "utf8").trim();
112
+ const pid = parseInt(pidContent, 10);
113
+ try {
114
+ process.kill(pid, 0);
115
+ } catch {
116
+ return defaultStatus;
117
+ }
118
+ if (!existsSync(statusFile)) {
119
+ return { ...defaultStatus, running: true, pid };
120
+ }
121
+ const content = readFileSync(statusFile, "utf8");
122
+ const status = JSON.parse(content);
123
+ return {
124
+ ...status,
125
+ running: true,
126
+ pid,
127
+ uptime: status.startTime ? Date.now() - status.startTime : void 0
128
+ };
129
+ } catch {
130
+ return defaultStatus;
131
+ }
132
+ }
133
+ function writeDaemonStatus(status) {
134
+ const { statusFile } = getDaemonPaths();
135
+ const currentStatus = readDaemonStatus();
136
+ const newStatus = { ...currentStatus, ...status };
137
+ writeFileSync(statusFile, JSON.stringify(newStatus, null, 2));
138
+ }
139
+ export {
140
+ DEFAULT_DAEMON_CONFIG,
141
+ getDaemonDir,
142
+ getDaemonPaths,
143
+ getLogsDir,
144
+ loadDaemonConfig,
145
+ readDaemonStatus,
146
+ saveDaemonConfig,
147
+ writeDaemonStatus
148
+ };
149
+ //# sourceMappingURL=daemon-config.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/daemon/daemon-config.ts"],
4
+ "sourcesContent": ["/**\n * Daemon Configuration Management\n * Handles loading, saving, and validating daemon configuration\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nexport interface DaemonServiceConfig {\n enabled: boolean;\n interval: number; // minutes\n}\n\nexport interface ContextServiceConfig extends DaemonServiceConfig {\n checkpointMessage?: string;\n}\n\nexport interface LinearServiceConfig extends DaemonServiceConfig {\n quietHours?: {\n start: number; // hour 0-23\n end: number;\n };\n retryAttempts: number;\n retryDelay: number; // ms\n}\n\nexport interface FileWatchConfig extends DaemonServiceConfig {\n paths: string[];\n extensions: string[];\n ignore: string[];\n debounceMs: number;\n}\n\nexport interface DaemonConfig {\n version: string;\n context: ContextServiceConfig;\n linear: LinearServiceConfig;\n fileWatch: FileWatchConfig;\n heartbeatInterval: number; // seconds\n inactivityTimeout: number; // minutes, 0 = disabled\n logLevel: 'debug' | 'info' | 'warn' | 'error';\n}\n\nexport const DEFAULT_DAEMON_CONFIG: DaemonConfig = {\n version: '1.0.0',\n context: {\n enabled: true,\n interval: 15, // 15 minutes\n checkpointMessage: 'Auto-checkpoint',\n },\n linear: {\n enabled: false, // Disabled by default, requires setup\n interval: 60, // 60 minutes\n quietHours: { start: 22, end: 7 },\n retryAttempts: 3,\n retryDelay: 30000,\n },\n fileWatch: {\n enabled: false, // Disabled by default\n interval: 0, // Not interval-based\n paths: ['.'],\n extensions: ['.ts', '.js', '.tsx', '.jsx', '.py', '.go', '.rs'],\n ignore: ['node_modules', '.git', 'dist', 'build', '.stackmemory'],\n debounceMs: 2000,\n },\n heartbeatInterval: 60, // 1 minute\n inactivityTimeout: 0, // Disabled by default\n logLevel: 'info',\n};\n\nexport interface DaemonStatus {\n running: boolean;\n pid?: number;\n startTime?: number;\n uptime?: number;\n services: {\n context: { enabled: boolean; lastRun?: number; saveCount?: number };\n linear: { enabled: boolean; lastRun?: number; syncCount?: number };\n fileWatch: { enabled: boolean; eventsProcessed?: number };\n };\n errors: string[];\n}\n\n/**\n * Get the daemon directory path\n */\nexport function getDaemonDir(): string {\n const dir = join(homedir(), '.stackmemory', 'daemon');\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n return dir;\n}\n\n/**\n * Get the logs directory path\n */\nexport function getLogsDir(): string {\n const dir = join(homedir(), '.stackmemory', 'logs');\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n return dir;\n}\n\n/**\n * Get daemon file paths\n */\nexport function getDaemonPaths() {\n const daemonDir = getDaemonDir();\n const logsDir = getLogsDir();\n return {\n pidFile: join(daemonDir, 'daemon.pid'),\n statusFile: join(daemonDir, 'daemon.status'),\n configFile: join(daemonDir, 'config.json'),\n logFile: join(logsDir, 'daemon.log'),\n };\n}\n\n/**\n * Load daemon configuration\n */\nexport function loadDaemonConfig(): DaemonConfig {\n const { configFile } = getDaemonPaths();\n\n if (!existsSync(configFile)) {\n return { ...DEFAULT_DAEMON_CONFIG };\n }\n\n try {\n const content = readFileSync(configFile, 'utf8');\n const config = JSON.parse(content) as Partial<DaemonConfig>;\n return {\n ...DEFAULT_DAEMON_CONFIG,\n ...config,\n context: { ...DEFAULT_DAEMON_CONFIG.context, ...config.context },\n linear: { ...DEFAULT_DAEMON_CONFIG.linear, ...config.linear },\n fileWatch: { ...DEFAULT_DAEMON_CONFIG.fileWatch, ...config.fileWatch },\n };\n } catch {\n return { ...DEFAULT_DAEMON_CONFIG };\n }\n}\n\n/**\n * Save daemon configuration\n */\nexport function saveDaemonConfig(config: Partial<DaemonConfig>): void {\n const { configFile } = getDaemonPaths();\n const currentConfig = loadDaemonConfig();\n const newConfig = {\n ...currentConfig,\n ...config,\n context: { ...currentConfig.context, ...config.context },\n linear: { ...currentConfig.linear, ...config.linear },\n fileWatch: { ...currentConfig.fileWatch, ...config.fileWatch },\n };\n writeFileSync(configFile, JSON.stringify(newConfig, null, 2));\n}\n\n/**\n * Read daemon status\n */\nexport function readDaemonStatus(): DaemonStatus {\n const { statusFile, pidFile } = getDaemonPaths();\n\n const defaultStatus: DaemonStatus = {\n running: false,\n services: {\n context: { enabled: false },\n linear: { enabled: false },\n fileWatch: { enabled: false },\n },\n errors: [],\n };\n\n // Check PID file first\n if (!existsSync(pidFile)) {\n return defaultStatus;\n }\n\n try {\n const pidContent = readFileSync(pidFile, 'utf8').trim();\n const pid = parseInt(pidContent, 10);\n\n // Check if process is running\n try {\n process.kill(pid, 0);\n } catch {\n // Process not running\n return defaultStatus;\n }\n\n // Read status file\n if (!existsSync(statusFile)) {\n return { ...defaultStatus, running: true, pid };\n }\n\n const content = readFileSync(statusFile, 'utf8');\n const status = JSON.parse(content) as DaemonStatus;\n return {\n ...status,\n running: true,\n pid,\n uptime: status.startTime ? Date.now() - status.startTime : undefined,\n };\n } catch {\n return defaultStatus;\n }\n}\n\n/**\n * Write daemon status\n */\nexport function writeDaemonStatus(status: Partial<DaemonStatus>): void {\n const { statusFile } = getDaemonPaths();\n const currentStatus = readDaemonStatus();\n const newStatus = { ...currentStatus, ...status };\n writeFileSync(statusFile, JSON.stringify(newStatus, null, 2));\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,122 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { existsSync } from "fs";
6
+ import { join } from "path";
7
+ import { execSync } from "child_process";
8
+ import { homedir } from "os";
9
+ class DaemonContextService {
10
+ config;
11
+ state;
12
+ intervalId;
13
+ isRunning = false;
14
+ onLog;
15
+ constructor(config, onLog) {
16
+ this.config = config;
17
+ this.onLog = onLog;
18
+ this.state = {
19
+ lastSaveTime: 0,
20
+ saveCount: 0,
21
+ errors: []
22
+ };
23
+ }
24
+ start() {
25
+ if (this.isRunning || !this.config.enabled) {
26
+ return;
27
+ }
28
+ this.isRunning = true;
29
+ const intervalMs = this.config.interval * 60 * 1e3;
30
+ this.onLog("INFO", "Context service started", {
31
+ interval: this.config.interval
32
+ });
33
+ this.saveContext();
34
+ this.intervalId = setInterval(() => {
35
+ this.saveContext();
36
+ }, intervalMs);
37
+ }
38
+ stop() {
39
+ if (this.intervalId) {
40
+ clearInterval(this.intervalId);
41
+ this.intervalId = void 0;
42
+ }
43
+ this.isRunning = false;
44
+ this.onLog("INFO", "Context service stopped");
45
+ }
46
+ getState() {
47
+ return { ...this.state };
48
+ }
49
+ updateConfig(config) {
50
+ const wasRunning = this.isRunning;
51
+ if (wasRunning) {
52
+ this.stop();
53
+ }
54
+ this.config = { ...this.config, ...config };
55
+ if (wasRunning && this.config.enabled) {
56
+ this.start();
57
+ }
58
+ }
59
+ forceSave() {
60
+ this.saveContext();
61
+ }
62
+ saveContext() {
63
+ if (!this.isRunning) return;
64
+ try {
65
+ const stackmemoryBin = this.getStackMemoryBin();
66
+ if (!stackmemoryBin) {
67
+ this.onLog("WARN", "StackMemory binary not found");
68
+ return;
69
+ }
70
+ const message = this.config.checkpointMessage || `Auto-checkpoint #${this.state.saveCount + 1}`;
71
+ const fullMessage = `${message} at ${(/* @__PURE__ */ new Date()).toISOString()}`;
72
+ execSync(`"${stackmemoryBin}" context add observation "${fullMessage}"`, {
73
+ timeout: 3e4,
74
+ encoding: "utf8",
75
+ stdio: "pipe"
76
+ });
77
+ this.state.saveCount++;
78
+ this.state.lastSaveTime = Date.now();
79
+ this.onLog("INFO", "Context saved", {
80
+ saveCount: this.state.saveCount
81
+ });
82
+ } catch (err) {
83
+ const errorMsg = err instanceof Error ? err.message : String(err);
84
+ if (!errorMsg.includes("EBUSY") && !errorMsg.includes("EAGAIN")) {
85
+ this.state.errors.push(errorMsg);
86
+ this.onLog("WARN", "Failed to save context", { error: errorMsg });
87
+ if (this.state.errors.length > 10) {
88
+ this.state.errors = this.state.errors.slice(-10);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ getStackMemoryBin() {
94
+ const homeDir = homedir();
95
+ const locations = [
96
+ join(homeDir, ".stackmemory", "bin", "stackmemory"),
97
+ join(homeDir, ".local", "bin", "stackmemory"),
98
+ "/usr/local/bin/stackmemory",
99
+ "/opt/homebrew/bin/stackmemory"
100
+ ];
101
+ for (const loc of locations) {
102
+ if (existsSync(loc)) {
103
+ return loc;
104
+ }
105
+ }
106
+ try {
107
+ const result = execSync("which stackmemory", {
108
+ encoding: "utf8",
109
+ stdio: "pipe"
110
+ }).trim();
111
+ if (result && existsSync(result)) {
112
+ return result;
113
+ }
114
+ } catch {
115
+ }
116
+ return null;
117
+ }
118
+ }
119
+ export {
120
+ DaemonContextService
121
+ };
122
+ //# sourceMappingURL=context-service.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/daemon/services/context-service.ts"],
4
+ "sourcesContent": ["/**\n * Context Auto-Save Service\n * Periodically saves context checkpoints\n */\n\nimport { existsSync } from 'fs';\nimport { join } from 'path';\nimport { execSync } from 'child_process';\nimport { homedir } from 'os';\nimport type { ContextServiceConfig } from '../daemon-config.js';\n\nexport interface ContextServiceState {\n lastSaveTime: number;\n saveCount: number;\n errors: string[];\n}\n\nexport class DaemonContextService {\n private config: ContextServiceConfig;\n private state: ContextServiceState;\n private intervalId?: NodeJS.Timeout;\n private isRunning = false;\n private onLog: (level: string, message: string, data?: unknown) => void;\n\n constructor(\n config: ContextServiceConfig,\n onLog: (level: string, message: string, data?: unknown) => void\n ) {\n this.config = config;\n this.onLog = onLog;\n this.state = {\n lastSaveTime: 0,\n saveCount: 0,\n errors: [],\n };\n }\n\n start(): void {\n if (this.isRunning || !this.config.enabled) {\n return;\n }\n\n this.isRunning = true;\n const intervalMs = this.config.interval * 60 * 1000;\n\n this.onLog('INFO', 'Context service started', {\n interval: this.config.interval,\n });\n\n // Initial save\n this.saveContext();\n\n // Schedule periodic saves\n this.intervalId = setInterval(() => {\n this.saveContext();\n }, intervalMs);\n }\n\n stop(): void {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = undefined;\n }\n this.isRunning = false;\n this.onLog('INFO', 'Context service stopped');\n }\n\n getState(): ContextServiceState {\n return { ...this.state };\n }\n\n updateConfig(config: Partial<ContextServiceConfig>): void {\n const wasRunning = this.isRunning;\n if (wasRunning) {\n this.stop();\n }\n\n this.config = { ...this.config, ...config };\n\n if (wasRunning && this.config.enabled) {\n this.start();\n }\n }\n\n forceSave(): void {\n this.saveContext();\n }\n\n private saveContext(): void {\n if (!this.isRunning) return;\n\n try {\n const stackmemoryBin = this.getStackMemoryBin();\n\n if (!stackmemoryBin) {\n this.onLog('WARN', 'StackMemory binary not found');\n return;\n }\n\n const message =\n this.config.checkpointMessage ||\n `Auto-checkpoint #${this.state.saveCount + 1}`;\n const fullMessage = `${message} at ${new Date().toISOString()}`;\n\n execSync(`\"${stackmemoryBin}\" context add observation \"${fullMessage}\"`, {\n timeout: 30000,\n encoding: 'utf8',\n stdio: 'pipe',\n });\n\n this.state.saveCount++;\n this.state.lastSaveTime = Date.now();\n\n this.onLog('INFO', 'Context saved', {\n saveCount: this.state.saveCount,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n\n // Only log if not a transient error\n if (!errorMsg.includes('EBUSY') && !errorMsg.includes('EAGAIN')) {\n this.state.errors.push(errorMsg);\n this.onLog('WARN', 'Failed to save context', { error: errorMsg });\n\n // Keep only last 10 errors\n if (this.state.errors.length > 10) {\n this.state.errors = this.state.errors.slice(-10);\n }\n }\n }\n }\n\n private getStackMemoryBin(): string | null {\n const homeDir = homedir();\n\n // Check common locations\n const locations = [\n join(homeDir, '.stackmemory', 'bin', 'stackmemory'),\n join(homeDir, '.local', 'bin', 'stackmemory'),\n '/usr/local/bin/stackmemory',\n '/opt/homebrew/bin/stackmemory',\n ];\n\n for (const loc of locations) {\n if (existsSync(loc)) {\n return loc;\n }\n }\n\n // Try to find in PATH\n try {\n const result = execSync('which stackmemory', {\n encoding: 'utf8',\n stdio: 'pipe',\n }).trim();\n if (result && existsSync(result)) {\n return result;\n }\n } catch {\n // Not in PATH\n }\n\n return null;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,136 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { existsSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+ class DaemonLinearService {
9
+ config;
10
+ state;
11
+ intervalId;
12
+ isRunning = false;
13
+ onLog;
14
+ constructor(config, onLog) {
15
+ this.config = config;
16
+ this.onLog = onLog;
17
+ this.state = {
18
+ lastSyncTime: 0,
19
+ syncCount: 0,
20
+ errors: []
21
+ };
22
+ }
23
+ async start() {
24
+ if (this.isRunning || !this.config.enabled) {
25
+ return;
26
+ }
27
+ if (!this.isLinearConfigured()) {
28
+ this.onLog("WARN", "Linear not configured, skipping linear service");
29
+ return;
30
+ }
31
+ this.isRunning = true;
32
+ const intervalMs = this.config.interval * 60 * 1e3;
33
+ this.onLog("INFO", "Linear service started", {
34
+ interval: this.config.interval,
35
+ quietHours: this.config.quietHours
36
+ });
37
+ await this.performSync();
38
+ this.intervalId = setInterval(async () => {
39
+ await this.performSync();
40
+ }, intervalMs);
41
+ }
42
+ stop() {
43
+ if (this.intervalId) {
44
+ clearInterval(this.intervalId);
45
+ this.intervalId = void 0;
46
+ }
47
+ this.isRunning = false;
48
+ this.onLog("INFO", "Linear service stopped");
49
+ }
50
+ getState() {
51
+ return {
52
+ ...this.state,
53
+ nextSyncTime: this.isRunning ? this.state.lastSyncTime + this.config.interval * 60 * 1e3 : void 0
54
+ };
55
+ }
56
+ updateConfig(config) {
57
+ const wasRunning = this.isRunning;
58
+ if (wasRunning) {
59
+ this.stop();
60
+ }
61
+ this.config = { ...this.config, ...config };
62
+ if (wasRunning && this.config.enabled) {
63
+ this.start();
64
+ }
65
+ }
66
+ async forceSync() {
67
+ await this.performSync();
68
+ }
69
+ async performSync() {
70
+ if (!this.isRunning) return;
71
+ if (this.isInQuietHours()) {
72
+ this.onLog("DEBUG", "Skipping sync during quiet hours");
73
+ return;
74
+ }
75
+ try {
76
+ const { LinearAutoSyncService } = await import("../../integrations/linear/auto-sync.js");
77
+ const projectRoot = this.findProjectRoot();
78
+ if (!projectRoot) {
79
+ this.onLog("WARN", "No project root found for Linear sync");
80
+ return;
81
+ }
82
+ const syncService = new LinearAutoSyncService(projectRoot, {
83
+ enabled: true,
84
+ interval: this.config.interval,
85
+ retryAttempts: this.config.retryAttempts,
86
+ retryDelay: this.config.retryDelay,
87
+ quietHours: this.config.quietHours
88
+ });
89
+ await syncService.forceSync();
90
+ syncService.stop();
91
+ this.state.syncCount++;
92
+ this.state.lastSyncTime = Date.now();
93
+ this.onLog("INFO", "Linear sync completed", {
94
+ syncCount: this.state.syncCount
95
+ });
96
+ } catch (err) {
97
+ const errorMsg = err instanceof Error ? err.message : String(err);
98
+ this.state.errors.push(errorMsg);
99
+ this.onLog("ERROR", "Linear sync failed", { error: errorMsg });
100
+ if (this.state.errors.length > 10) {
101
+ this.state.errors = this.state.errors.slice(-10);
102
+ }
103
+ }
104
+ }
105
+ isLinearConfigured() {
106
+ const homeDir = homedir();
107
+ const configPath = join(homeDir, ".stackmemory", "linear-auth.json");
108
+ return existsSync(configPath) || !!process.env["LINEAR_API_KEY"];
109
+ }
110
+ findProjectRoot() {
111
+ const cwd = process.cwd();
112
+ if (existsSync(join(cwd, ".stackmemory"))) {
113
+ return cwd;
114
+ }
115
+ const homeDir = homedir();
116
+ if (existsSync(join(homeDir, ".stackmemory"))) {
117
+ return homeDir;
118
+ }
119
+ return null;
120
+ }
121
+ isInQuietHours() {
122
+ if (!this.config.quietHours) return false;
123
+ const now = /* @__PURE__ */ new Date();
124
+ const currentHour = now.getHours();
125
+ const { start, end } = this.config.quietHours;
126
+ if (start > end) {
127
+ return currentHour >= start || currentHour < end;
128
+ } else {
129
+ return currentHour >= start && currentHour < end;
130
+ }
131
+ }
132
+ }
133
+ export {
134
+ DaemonLinearService
135
+ };
136
+ //# sourceMappingURL=linear-service.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/daemon/services/linear-service.ts"],
4
+ "sourcesContent": ["/**\n * Linear Sync Service Wrapper\n * Wraps LinearAutoSyncService for daemon integration\n */\n\nimport { existsSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport type { LinearServiceConfig } from '../daemon-config.js';\n\nexport interface LinearServiceState {\n lastSyncTime: number;\n syncCount: number;\n errors: string[];\n nextSyncTime?: number;\n}\n\nexport class DaemonLinearService {\n private config: LinearServiceConfig;\n private state: LinearServiceState;\n private intervalId?: NodeJS.Timeout;\n private isRunning = false;\n private onLog: (level: string, message: string, data?: unknown) => void;\n\n constructor(\n config: LinearServiceConfig,\n onLog: (level: string, message: string, data?: unknown) => void\n ) {\n this.config = config;\n this.onLog = onLog;\n this.state = {\n lastSyncTime: 0,\n syncCount: 0,\n errors: [],\n };\n }\n\n async start(): Promise<void> {\n if (this.isRunning || !this.config.enabled) {\n return;\n }\n\n // Check if Linear is configured\n if (!this.isLinearConfigured()) {\n this.onLog('WARN', 'Linear not configured, skipping linear service');\n return;\n }\n\n this.isRunning = true;\n const intervalMs = this.config.interval * 60 * 1000;\n\n this.onLog('INFO', 'Linear service started', {\n interval: this.config.interval,\n quietHours: this.config.quietHours,\n });\n\n // Initial sync\n await this.performSync();\n\n // Schedule periodic syncs\n this.intervalId = setInterval(async () => {\n await this.performSync();\n }, intervalMs);\n }\n\n stop(): void {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = undefined;\n }\n this.isRunning = false;\n this.onLog('INFO', 'Linear service stopped');\n }\n\n getState(): LinearServiceState {\n return {\n ...this.state,\n nextSyncTime: this.isRunning\n ? this.state.lastSyncTime + this.config.interval * 60 * 1000\n : undefined,\n };\n }\n\n updateConfig(config: Partial<LinearServiceConfig>): void {\n const wasRunning = this.isRunning;\n if (wasRunning) {\n this.stop();\n }\n\n this.config = { ...this.config, ...config };\n\n if (wasRunning && this.config.enabled) {\n this.start();\n }\n }\n\n async forceSync(): Promise<void> {\n await this.performSync();\n }\n\n private async performSync(): Promise<void> {\n if (!this.isRunning) return;\n\n // Check quiet hours\n if (this.isInQuietHours()) {\n this.onLog('DEBUG', 'Skipping sync during quiet hours');\n return;\n }\n\n try {\n // Dynamically import LinearAutoSyncService to avoid loading if not needed\n const { LinearAutoSyncService } =\n await import('../../integrations/linear/auto-sync.js');\n\n const projectRoot = this.findProjectRoot();\n if (!projectRoot) {\n this.onLog('WARN', 'No project root found for Linear sync');\n return;\n }\n\n const syncService = new LinearAutoSyncService(projectRoot, {\n enabled: true,\n interval: this.config.interval,\n retryAttempts: this.config.retryAttempts,\n retryDelay: this.config.retryDelay,\n quietHours: this.config.quietHours,\n });\n\n await syncService.forceSync();\n syncService.stop();\n\n this.state.syncCount++;\n this.state.lastSyncTime = Date.now();\n\n this.onLog('INFO', 'Linear sync completed', {\n syncCount: this.state.syncCount,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n this.state.errors.push(errorMsg);\n this.onLog('ERROR', 'Linear sync failed', { error: errorMsg });\n\n // Keep only last 10 errors\n if (this.state.errors.length > 10) {\n this.state.errors = this.state.errors.slice(-10);\n }\n }\n }\n\n private isLinearConfigured(): boolean {\n const homeDir = homedir();\n const configPath = join(homeDir, '.stackmemory', 'linear-auth.json');\n return existsSync(configPath) || !!process.env['LINEAR_API_KEY'];\n }\n\n private findProjectRoot(): string | null {\n // Check common locations\n const cwd = process.cwd();\n if (existsSync(join(cwd, '.stackmemory'))) {\n return cwd;\n }\n\n // Check home directory\n const homeDir = homedir();\n if (existsSync(join(homeDir, '.stackmemory'))) {\n return homeDir;\n }\n\n return null;\n }\n\n private isInQuietHours(): boolean {\n if (!this.config.quietHours) return false;\n\n const now = new Date();\n const currentHour = now.getHours();\n const { start, end } = this.config.quietHours;\n\n if (start > end) {\n // Quiet hours span midnight (e.g., 22:00 - 07:00)\n return currentHour >= start || currentHour < end;\n } else {\n // Quiet hours within same day\n return currentHour >= start && currentHour < end;\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }