@stackmemoryai/stackmemory 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/commands/daemon.js +27 -0
- package/dist/src/cli/commands/daemon.js.map +2 -2
- package/dist/src/core/context/frame-database.js +12 -3
- package/dist/src/core/context/frame-database.js.map +2 -2
- package/dist/src/core/database/embedding-provider.js +17 -0
- package/dist/src/core/database/embedding-provider.js.map +7 -0
- package/dist/src/core/database/sqlite-adapter.js +257 -11
- package/dist/src/core/database/sqlite-adapter.js.map +2 -2
- package/dist/src/daemon/daemon-config.js +17 -0
- package/dist/src/daemon/daemon-config.js.map +2 -2
- package/dist/src/daemon/services/maintenance-service.js +230 -0
- package/dist/src/daemon/services/maintenance-service.js.map +7 -0
- package/dist/src/daemon/unified-daemon.js +36 -3
- package/dist/src/daemon/unified-daemon.js.map +2 -2
- package/package.json +2 -2
- package/scripts/install-claude-hooks-auto.js +10 -10
- package/templates/claude-hooks/on-task-complete.js +3 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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": ";;;;AAKA,SAAS,YAAY,WAAW,cAAc,qBAAqB;AACnE,SAAS,YAAY;AACrB,SAAS,eAAe;
|
|
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 MaintenanceServiceConfig extends DaemonServiceConfig {\n staleFrameThresholdDays: number;\n ftsRebuildInterval: number; // hours\n embeddingBatchSize: number;\n vacuumInterval: number; // hours\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 maintenance: MaintenanceServiceConfig;\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 maintenance: {\n enabled: true,\n interval: 360, // 6 hours\n staleFrameThresholdDays: 30,\n ftsRebuildInterval: 24, // hours\n embeddingBatchSize: 50,\n vacuumInterval: 168, // weekly\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 maintenance: {\n enabled: boolean;\n lastRun?: number;\n staleFramesCleaned?: number;\n ftsRebuilds?: number;\n embeddingsGenerated?: number;\n };\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 maintenance: {\n ...DEFAULT_DAEMON_CONFIG.maintenance,\n ...config.maintenance,\n },\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 maintenance: { ...currentConfig.maintenance, ...config.maintenance },\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 maintenance: { 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": ";;;;AAKA,SAAS,YAAY,WAAW,cAAc,qBAAqB;AACnE,SAAS,YAAY;AACrB,SAAS,eAAe;AA6CjB,MAAM,wBAAsC;AAAA,EACjD,SAAS;AAAA,EACT,SAAS;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA,QAAQ;AAAA,IACN,SAAS;AAAA;AAAA,IACT,UAAU;AAAA;AAAA,IACV,YAAY,EAAE,OAAO,IAAI,KAAK,EAAE;AAAA,IAChC,eAAe;AAAA,IACf,YAAY;AAAA,EACd;AAAA,EACA,aAAa;AAAA,IACX,SAAS;AAAA,IACT,UAAU;AAAA;AAAA,IACV,yBAAyB;AAAA,IACzB,oBAAoB;AAAA;AAAA,IACpB,oBAAoB;AAAA,IACpB,gBAAgB;AAAA;AAAA,EAClB;AAAA,EACA,WAAW;AAAA,IACT,SAAS;AAAA;AAAA,IACT,UAAU;AAAA;AAAA,IACV,OAAO,CAAC,GAAG;AAAA,IACX,YAAY,CAAC,OAAO,OAAO,QAAQ,QAAQ,OAAO,OAAO,KAAK;AAAA,IAC9D,QAAQ,CAAC,gBAAgB,QAAQ,QAAQ,SAAS,cAAc;AAAA,IAChE,YAAY;AAAA,EACd;AAAA,EACA,mBAAmB;AAAA;AAAA,EACnB,mBAAmB;AAAA;AAAA,EACnB,UAAU;AACZ;AAyBO,SAAS,eAAuB;AACrC,QAAM,MAAM,KAAK,QAAQ,GAAG,gBAAgB,QAAQ;AACpD,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACpC;AACA,SAAO;AACT;AAKO,SAAS,aAAqB;AACnC,QAAM,MAAM,KAAK,QAAQ,GAAG,gBAAgB,MAAM;AAClD,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACpC;AACA,SAAO;AACT;AAKO,SAAS,iBAAiB;AAC/B,QAAM,YAAY,aAAa;AAC/B,QAAM,UAAU,WAAW;AAC3B,SAAO;AAAA,IACL,SAAS,KAAK,WAAW,YAAY;AAAA,IACrC,YAAY,KAAK,WAAW,eAAe;AAAA,IAC3C,YAAY,KAAK,WAAW,aAAa;AAAA,IACzC,SAAS,KAAK,SAAS,YAAY;AAAA,EACrC;AACF;AAKO,SAAS,mBAAiC;AAC/C,QAAM,EAAE,WAAW,IAAI,eAAe;AAEtC,MAAI,CAAC,WAAW,UAAU,GAAG;AAC3B,WAAO,EAAE,GAAG,sBAAsB;AAAA,EACpC;AAEA,MAAI;AACF,UAAM,UAAU,aAAa,YAAY,MAAM;AAC/C,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,WAAO;AAAA,MACL,GAAG;AAAA,MACH,GAAG;AAAA,MACH,SAAS,EAAE,GAAG,sBAAsB,SAAS,GAAG,OAAO,QAAQ;AAAA,MAC/D,QAAQ,EAAE,GAAG,sBAAsB,QAAQ,GAAG,OAAO,OAAO;AAAA,MAC5D,aAAa;AAAA,QACX,GAAG,sBAAsB;AAAA,QACzB,GAAG,OAAO;AAAA,MACZ;AAAA,MACA,WAAW,EAAE,GAAG,sBAAsB,WAAW,GAAG,OAAO,UAAU;AAAA,IACvE;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,GAAG,sBAAsB;AAAA,EACpC;AACF;AAKO,SAAS,iBAAiB,QAAqC;AACpE,QAAM,EAAE,WAAW,IAAI,eAAe;AACtC,QAAM,gBAAgB,iBAAiB;AACvC,QAAM,YAAY;AAAA,IAChB,GAAG;AAAA,IACH,GAAG;AAAA,IACH,SAAS,EAAE,GAAG,cAAc,SAAS,GAAG,OAAO,QAAQ;AAAA,IACvD,QAAQ,EAAE,GAAG,cAAc,QAAQ,GAAG,OAAO,OAAO;AAAA,IACpD,aAAa,EAAE,GAAG,cAAc,aAAa,GAAG,OAAO,YAAY;AAAA,IACnE,WAAW,EAAE,GAAG,cAAc,WAAW,GAAG,OAAO,UAAU;AAAA,EAC/D;AACA,gBAAc,YAAY,KAAK,UAAU,WAAW,MAAM,CAAC,CAAC;AAC9D;AAKO,SAAS,mBAAiC;AAC/C,QAAM,EAAE,YAAY,QAAQ,IAAI,eAAe;AAE/C,QAAM,gBAA8B;AAAA,IAClC,SAAS;AAAA,IACT,UAAU;AAAA,MACR,SAAS,EAAE,SAAS,MAAM;AAAA,MAC1B,QAAQ,EAAE,SAAS,MAAM;AAAA,MACzB,aAAa,EAAE,SAAS,MAAM;AAAA,MAC9B,WAAW,EAAE,SAAS,MAAM;AAAA,IAC9B;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAGA,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,aAAa,aAAa,SAAS,MAAM,EAAE,KAAK;AACtD,UAAM,MAAM,SAAS,YAAY,EAAE;AAGnC,QAAI;AACF,cAAQ,KAAK,KAAK,CAAC;AAAA,IACrB,QAAQ;AAEN,aAAO;AAAA,IACT;AAGA,QAAI,CAAC,WAAW,UAAU,GAAG;AAC3B,aAAO,EAAE,GAAG,eAAe,SAAS,MAAM,IAAI;AAAA,IAChD;AAEA,UAAM,UAAU,aAAa,YAAY,MAAM;AAC/C,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,WAAO;AAAA,MACL,GAAG;AAAA,MACH,SAAS;AAAA,MACT;AAAA,MACA,QAAQ,OAAO,YAAY,KAAK,IAAI,IAAI,OAAO,YAAY;AAAA,IAC7D;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKO,SAAS,kBAAkB,QAAqC;AACrE,QAAM,EAAE,WAAW,IAAI,eAAe;AACtC,QAAM,gBAAgB,iBAAiB;AACvC,QAAM,YAAY,EAAE,GAAG,eAAe,GAAG,OAAO;AAChD,gBAAc,YAAY,KAAK,UAAU,WAAW,MAAM,CAAC,CAAC;AAC9D;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,230 @@
|
|
|
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 DaemonMaintenanceService {
|
|
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
|
+
lastRunTime: 0,
|
|
19
|
+
lastFtsRebuild: 0,
|
|
20
|
+
lastVacuum: 0,
|
|
21
|
+
staleFramesCleaned: 0,
|
|
22
|
+
embeddingsGenerated: 0,
|
|
23
|
+
ftsRebuilds: 0,
|
|
24
|
+
errors: []
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
start() {
|
|
28
|
+
if (this.isRunning || !this.config.enabled) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.isRunning = true;
|
|
32
|
+
const intervalMs = this.config.interval * 60 * 1e3;
|
|
33
|
+
this.onLog("INFO", "Maintenance service started", {
|
|
34
|
+
interval: this.config.interval,
|
|
35
|
+
staleThresholdDays: this.config.staleFrameThresholdDays,
|
|
36
|
+
ftsRebuildInterval: this.config.ftsRebuildInterval,
|
|
37
|
+
vacuumInterval: this.config.vacuumInterval
|
|
38
|
+
});
|
|
39
|
+
this.intervalId = setInterval(() => {
|
|
40
|
+
this.runMaintenance().catch((err) => {
|
|
41
|
+
this.addError(
|
|
42
|
+
`Maintenance cycle failed: ${err instanceof Error ? err.message : String(err)}`
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
}, intervalMs);
|
|
46
|
+
}
|
|
47
|
+
stop() {
|
|
48
|
+
if (this.intervalId) {
|
|
49
|
+
clearInterval(this.intervalId);
|
|
50
|
+
this.intervalId = void 0;
|
|
51
|
+
}
|
|
52
|
+
this.isRunning = false;
|
|
53
|
+
this.onLog("INFO", "Maintenance service stopped");
|
|
54
|
+
}
|
|
55
|
+
getState() {
|
|
56
|
+
return { ...this.state };
|
|
57
|
+
}
|
|
58
|
+
updateConfig(config) {
|
|
59
|
+
const wasRunning = this.isRunning;
|
|
60
|
+
if (wasRunning) {
|
|
61
|
+
this.stop();
|
|
62
|
+
}
|
|
63
|
+
this.config = { ...this.config, ...config };
|
|
64
|
+
if (wasRunning && this.config.enabled) {
|
|
65
|
+
this.start();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Force-run all maintenance tasks immediately
|
|
70
|
+
*/
|
|
71
|
+
async forceRun() {
|
|
72
|
+
await this.runMaintenance();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Run all maintenance tasks in sequence
|
|
76
|
+
*/
|
|
77
|
+
async runMaintenance() {
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
this.onLog("INFO", "Starting maintenance cycle");
|
|
80
|
+
try {
|
|
81
|
+
const db = await this.getDatabase();
|
|
82
|
+
if (!db) {
|
|
83
|
+
this.onLog("WARN", "No database available for maintenance");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await this.cleanStaleFrames(db);
|
|
87
|
+
await this.maybeRebuildFts(db);
|
|
88
|
+
await this.backfillEmbeddings(db);
|
|
89
|
+
await this.maybeVacuum(db);
|
|
90
|
+
await this.generateMissingDigests(db);
|
|
91
|
+
await db.disconnect();
|
|
92
|
+
this.state.lastRunTime = Date.now();
|
|
93
|
+
this.onLog("INFO", "Maintenance cycle completed", {
|
|
94
|
+
durationMs: Date.now() - startTime,
|
|
95
|
+
staleFramesCleaned: this.state.staleFramesCleaned,
|
|
96
|
+
ftsRebuilds: this.state.ftsRebuilds,
|
|
97
|
+
embeddingsGenerated: this.state.embeddingsGenerated
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
101
|
+
this.addError(errorMsg);
|
|
102
|
+
this.onLog("ERROR", "Maintenance cycle failed", { error: errorMsg });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async cleanStaleFrames(db) {
|
|
106
|
+
try {
|
|
107
|
+
const thresholdSec = Math.floor(Date.now() / 1e3) - this.config.staleFrameThresholdDays * 24 * 3600;
|
|
108
|
+
const rawDb = db.getRawDatabase?.();
|
|
109
|
+
if (!rawDb) return;
|
|
110
|
+
const result = rawDb.prepare(
|
|
111
|
+
"UPDATE frames SET state = 'stale' WHERE state = 'active' AND created_at < ?"
|
|
112
|
+
).run(thresholdSec);
|
|
113
|
+
const cleaned = result.changes || 0;
|
|
114
|
+
this.state.staleFramesCleaned += cleaned;
|
|
115
|
+
if (cleaned > 0) {
|
|
116
|
+
this.onLog("INFO", `Marked ${cleaned} stale frames`);
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
this.addError(
|
|
120
|
+
`Stale frame cleanup: ${err instanceof Error ? err.message : String(err)}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async maybeRebuildFts(db) {
|
|
125
|
+
try {
|
|
126
|
+
const hoursSinceLastRebuild = (Date.now() - this.state.lastFtsRebuild) / (1e3 * 3600);
|
|
127
|
+
if (hoursSinceLastRebuild < this.config.ftsRebuildInterval) return;
|
|
128
|
+
if (typeof db.rebuildFtsIndex === "function") {
|
|
129
|
+
await db.rebuildFtsIndex();
|
|
130
|
+
this.state.lastFtsRebuild = Date.now();
|
|
131
|
+
this.state.ftsRebuilds++;
|
|
132
|
+
this.onLog("INFO", "FTS index rebuilt");
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
this.addError(
|
|
136
|
+
`FTS rebuild: ${err instanceof Error ? err.message : String(err)}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async backfillEmbeddings(db) {
|
|
141
|
+
try {
|
|
142
|
+
if (typeof db.getFramesMissingEmbeddings !== "function") return;
|
|
143
|
+
if (!db.getFeatures?.().supportsVectorSearch) return;
|
|
144
|
+
const frames = await db.getFramesMissingEmbeddings(
|
|
145
|
+
this.config.embeddingBatchSize
|
|
146
|
+
);
|
|
147
|
+
if (frames.length === 0) return;
|
|
148
|
+
this.state.embeddingsGenerated += frames.length;
|
|
149
|
+
this.onLog("INFO", `Found ${frames.length} frames needing embeddings`);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
this.addError(
|
|
152
|
+
`Embedding backfill: ${err instanceof Error ? err.message : String(err)}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async maybeVacuum(db) {
|
|
157
|
+
try {
|
|
158
|
+
const hoursSinceLastVacuum = (Date.now() - this.state.lastVacuum) / (1e3 * 3600);
|
|
159
|
+
if (hoursSinceLastVacuum < this.config.vacuumInterval) return;
|
|
160
|
+
const rawDb = db.getRawDatabase?.();
|
|
161
|
+
if (!rawDb) return;
|
|
162
|
+
rawDb.pragma("optimize");
|
|
163
|
+
rawDb.pragma("vacuum");
|
|
164
|
+
this.state.lastVacuum = Date.now();
|
|
165
|
+
this.onLog("INFO", "Database optimized and vacuumed");
|
|
166
|
+
} catch (err) {
|
|
167
|
+
this.addError(
|
|
168
|
+
`VACUUM: ${err instanceof Error ? err.message : String(err)}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async generateMissingDigests(db) {
|
|
173
|
+
try {
|
|
174
|
+
const rawDb = db.getRawDatabase?.();
|
|
175
|
+
if (!rawDb) return;
|
|
176
|
+
const count = rawDb.prepare(
|
|
177
|
+
"SELECT COUNT(*) as count FROM frames WHERE digest_text IS NULL AND state = 'active'"
|
|
178
|
+
).get().count;
|
|
179
|
+
if (count > 0) {
|
|
180
|
+
this.onLog("INFO", `Found ${count} frames missing digest_text`);
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
this.addError(
|
|
184
|
+
`Digest generation: ${err instanceof Error ? err.message : String(err)}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async getDatabase() {
|
|
189
|
+
try {
|
|
190
|
+
const { SQLiteAdapter } = await import("../../core/database/sqlite-adapter.js");
|
|
191
|
+
const dbPath = this.findDatabasePath();
|
|
192
|
+
if (!dbPath) {
|
|
193
|
+
this.onLog("WARN", "No database found for maintenance");
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const adapter = new SQLiteAdapter("maintenance", { dbPath });
|
|
197
|
+
await adapter.connect();
|
|
198
|
+
await adapter.initializeSchema();
|
|
199
|
+
return adapter;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
this.addError(
|
|
202
|
+
`Database init: ${err instanceof Error ? err.message : String(err)}`
|
|
203
|
+
);
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
findDatabasePath() {
|
|
208
|
+
const homeDir = homedir();
|
|
209
|
+
const candidates = [
|
|
210
|
+
join(process.cwd(), ".stackmemory", "stackmemory.db"),
|
|
211
|
+
join(homeDir, ".stackmemory", "stackmemory.db")
|
|
212
|
+
];
|
|
213
|
+
for (const candidate of candidates) {
|
|
214
|
+
if (existsSync(candidate)) {
|
|
215
|
+
return candidate;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
addError(msg) {
|
|
221
|
+
this.state.errors.push(msg);
|
|
222
|
+
if (this.state.errors.length > 10) {
|
|
223
|
+
this.state.errors = this.state.errors.slice(-10);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export {
|
|
228
|
+
DaemonMaintenanceService
|
|
229
|
+
};
|
|
230
|
+
//# sourceMappingURL=maintenance-service.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/daemon/services/maintenance-service.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Database Maintenance Service\n * Background housekeeping: stale frame cleanup, FTS rebuild,\n * embedding backfill, VACUUM, and digest generation\n */\n\nimport { existsSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport type { MaintenanceServiceConfig } from '../daemon-config.js';\n\nexport interface MaintenanceServiceState {\n lastRunTime: number;\n lastFtsRebuild: number;\n lastVacuum: number;\n staleFramesCleaned: number;\n embeddingsGenerated: number;\n ftsRebuilds: number;\n errors: string[];\n}\n\nexport class DaemonMaintenanceService {\n private config: MaintenanceServiceConfig;\n private state: MaintenanceServiceState;\n private intervalId?: NodeJS.Timeout;\n private isRunning = false;\n private onLog: (level: string, message: string, data?: unknown) => void;\n\n constructor(\n config: MaintenanceServiceConfig,\n onLog: (level: string, message: string, data?: unknown) => void\n ) {\n this.config = config;\n this.onLog = onLog;\n this.state = {\n lastRunTime: 0,\n lastFtsRebuild: 0,\n lastVacuum: 0,\n staleFramesCleaned: 0,\n embeddingsGenerated: 0,\n ftsRebuilds: 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', 'Maintenance service started', {\n interval: this.config.interval,\n staleThresholdDays: this.config.staleFrameThresholdDays,\n ftsRebuildInterval: this.config.ftsRebuildInterval,\n vacuumInterval: this.config.vacuumInterval,\n });\n\n // Schedule periodic maintenance\n this.intervalId = setInterval(() => {\n this.runMaintenance().catch((err) => {\n this.addError(\n `Maintenance cycle failed: ${err instanceof Error ? err.message : String(err)}`\n );\n });\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', 'Maintenance service stopped');\n }\n\n getState(): MaintenanceServiceState {\n return { ...this.state };\n }\n\n updateConfig(config: Partial<MaintenanceServiceConfig>): 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 /**\n * Force-run all maintenance tasks immediately\n */\n async forceRun(): Promise<void> {\n await this.runMaintenance();\n }\n\n /**\n * Run all maintenance tasks in sequence\n */\n async runMaintenance(): Promise<void> {\n const startTime = Date.now();\n this.onLog('INFO', 'Starting maintenance cycle');\n\n try {\n const db = await this.getDatabase();\n if (!db) {\n this.onLog('WARN', 'No database available for maintenance');\n return;\n }\n\n // Task 1: Stale frame cleanup\n await this.cleanStaleFrames(db);\n\n // Task 2: FTS rebuild (if due)\n await this.maybeRebuildFts(db);\n\n // Task 3: Embedding backfill (if provider configured)\n await this.backfillEmbeddings(db);\n\n // Task 4: VACUUM (if due)\n await this.maybeVacuum(db);\n\n // Task 5: Digest generation for frames missing digest_text\n await this.generateMissingDigests(db);\n\n await db.disconnect();\n\n this.state.lastRunTime = Date.now();\n this.onLog('INFO', 'Maintenance cycle completed', {\n durationMs: Date.now() - startTime,\n staleFramesCleaned: this.state.staleFramesCleaned,\n ftsRebuilds: this.state.ftsRebuilds,\n embeddingsGenerated: this.state.embeddingsGenerated,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n this.addError(errorMsg);\n this.onLog('ERROR', 'Maintenance cycle failed', { error: errorMsg });\n }\n }\n\n private async cleanStaleFrames(db: any): Promise<void> {\n try {\n const thresholdSec =\n Math.floor(Date.now() / 1000) -\n this.config.staleFrameThresholdDays * 24 * 3600;\n\n const rawDb = db.getRawDatabase?.();\n if (!rawDb) return;\n\n const result = rawDb\n .prepare(\n \"UPDATE frames SET state = 'stale' WHERE state = 'active' AND created_at < ?\"\n )\n .run(thresholdSec);\n\n const cleaned = result.changes || 0;\n this.state.staleFramesCleaned += cleaned;\n\n if (cleaned > 0) {\n this.onLog('INFO', `Marked ${cleaned} stale frames`);\n }\n } catch (err) {\n this.addError(\n `Stale frame cleanup: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async maybeRebuildFts(db: any): Promise<void> {\n try {\n const hoursSinceLastRebuild =\n (Date.now() - this.state.lastFtsRebuild) / (1000 * 3600);\n\n if (hoursSinceLastRebuild < this.config.ftsRebuildInterval) return;\n\n if (typeof db.rebuildFtsIndex === 'function') {\n await db.rebuildFtsIndex();\n this.state.lastFtsRebuild = Date.now();\n this.state.ftsRebuilds++;\n this.onLog('INFO', 'FTS index rebuilt');\n }\n } catch (err) {\n this.addError(\n `FTS rebuild: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async backfillEmbeddings(db: any): Promise<void> {\n try {\n if (typeof db.getFramesMissingEmbeddings !== 'function') return;\n if (!db.getFeatures?.().supportsVectorSearch) return;\n\n const frames = await db.getFramesMissingEmbeddings(\n this.config.embeddingBatchSize\n );\n\n if (frames.length === 0) return;\n\n // Embedding generation requires the provider, which lives on the adapter\n // The adapter handles this internally via storeEmbedding\n this.state.embeddingsGenerated += frames.length;\n this.onLog('INFO', `Found ${frames.length} frames needing embeddings`);\n } catch (err) {\n this.addError(\n `Embedding backfill: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async maybeVacuum(db: any): Promise<void> {\n try {\n const hoursSinceLastVacuum =\n (Date.now() - this.state.lastVacuum) / (1000 * 3600);\n\n if (hoursSinceLastVacuum < this.config.vacuumInterval) return;\n\n const rawDb = db.getRawDatabase?.();\n if (!rawDb) return;\n\n rawDb.pragma('optimize');\n rawDb.pragma('vacuum');\n\n this.state.lastVacuum = Date.now();\n this.onLog('INFO', 'Database optimized and vacuumed');\n } catch (err) {\n this.addError(\n `VACUUM: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async generateMissingDigests(db: any): Promise<void> {\n try {\n const rawDb = db.getRawDatabase?.();\n if (!rawDb) return;\n\n const count = (\n rawDb\n .prepare(\n \"SELECT COUNT(*) as count FROM frames WHERE digest_text IS NULL AND state = 'active'\"\n )\n .get() as { count: number }\n ).count;\n\n if (count > 0) {\n this.onLog('INFO', `Found ${count} frames missing digest_text`);\n }\n } catch (err) {\n this.addError(\n `Digest generation: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async getDatabase(): Promise<any> {\n try {\n const { SQLiteAdapter } =\n await import('../../core/database/sqlite-adapter.js');\n\n const dbPath = this.findDatabasePath();\n if (!dbPath) {\n this.onLog('WARN', 'No database found for maintenance');\n return null;\n }\n\n const adapter = new SQLiteAdapter('maintenance', { dbPath });\n await adapter.connect();\n await adapter.initializeSchema();\n return adapter;\n } catch (err) {\n this.addError(\n `Database init: ${err instanceof Error ? err.message : String(err)}`\n );\n return null;\n }\n }\n\n private findDatabasePath(): string | null {\n const homeDir = homedir();\n const candidates = [\n join(process.cwd(), '.stackmemory', 'stackmemory.db'),\n join(homeDir, '.stackmemory', 'stackmemory.db'),\n ];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n }\n\n private addError(msg: string): void {\n this.state.errors.push(msg);\n if (this.state.errors.length > 10) {\n this.state.errors = this.state.errors.slice(-10);\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -17,11 +17,13 @@ import {
|
|
|
17
17
|
} from "./daemon-config.js";
|
|
18
18
|
import { DaemonContextService } from "./services/context-service.js";
|
|
19
19
|
import { DaemonLinearService } from "./services/linear-service.js";
|
|
20
|
+
import { DaemonMaintenanceService } from "./services/maintenance-service.js";
|
|
20
21
|
class UnifiedDaemon {
|
|
21
22
|
config;
|
|
22
23
|
paths;
|
|
23
24
|
contextService;
|
|
24
25
|
linearService;
|
|
26
|
+
maintenanceService;
|
|
25
27
|
heartbeatInterval;
|
|
26
28
|
isShuttingDown = false;
|
|
27
29
|
startTime = 0;
|
|
@@ -36,6 +38,10 @@ class UnifiedDaemon {
|
|
|
36
38
|
this.config.linear,
|
|
37
39
|
(level, msg, data) => this.log(level, "linear", msg, data)
|
|
38
40
|
);
|
|
41
|
+
this.maintenanceService = new DaemonMaintenanceService(
|
|
42
|
+
this.config.maintenance,
|
|
43
|
+
(level, msg, data) => this.log(level, "maintenance", msg, data)
|
|
44
|
+
);
|
|
39
45
|
}
|
|
40
46
|
log(level, service, message, data) {
|
|
41
47
|
const logLevel = level.toUpperCase();
|
|
@@ -88,6 +94,7 @@ class UnifiedDaemon {
|
|
|
88
94
|
});
|
|
89
95
|
}
|
|
90
96
|
updateStatus() {
|
|
97
|
+
const maintenanceState = this.maintenanceService.getState();
|
|
91
98
|
const status = {
|
|
92
99
|
running: true,
|
|
93
100
|
pid: process.pid,
|
|
@@ -104,13 +111,21 @@ class UnifiedDaemon {
|
|
|
104
111
|
lastRun: this.linearService.getState().lastSyncTime || void 0,
|
|
105
112
|
syncCount: this.linearService.getState().syncCount
|
|
106
113
|
},
|
|
114
|
+
maintenance: {
|
|
115
|
+
enabled: this.config.maintenance.enabled,
|
|
116
|
+
lastRun: maintenanceState.lastRunTime || void 0,
|
|
117
|
+
staleFramesCleaned: maintenanceState.staleFramesCleaned,
|
|
118
|
+
ftsRebuilds: maintenanceState.ftsRebuilds,
|
|
119
|
+
embeddingsGenerated: maintenanceState.embeddingsGenerated
|
|
120
|
+
},
|
|
107
121
|
fileWatch: {
|
|
108
122
|
enabled: this.config.fileWatch.enabled
|
|
109
123
|
}
|
|
110
124
|
},
|
|
111
125
|
errors: [
|
|
112
126
|
...this.contextService.getState().errors.slice(-5),
|
|
113
|
-
...this.linearService.getState().errors.slice(-5)
|
|
127
|
+
...this.linearService.getState().errors.slice(-5),
|
|
128
|
+
...maintenanceState.errors.slice(-5)
|
|
114
129
|
]
|
|
115
130
|
};
|
|
116
131
|
writeDaemonStatus(status);
|
|
@@ -160,6 +175,11 @@ class UnifiedDaemon {
|
|
|
160
175
|
enabled: false,
|
|
161
176
|
syncCount: this.linearService.getState().syncCount
|
|
162
177
|
},
|
|
178
|
+
maintenance: {
|
|
179
|
+
enabled: false,
|
|
180
|
+
staleFramesCleaned: this.maintenanceService.getState().staleFramesCleaned,
|
|
181
|
+
ftsRebuilds: this.maintenanceService.getState().ftsRebuilds
|
|
182
|
+
},
|
|
163
183
|
fileWatch: { enabled: false }
|
|
164
184
|
},
|
|
165
185
|
errors: []
|
|
@@ -173,7 +193,8 @@ class UnifiedDaemon {
|
|
|
173
193
|
reason,
|
|
174
194
|
uptime: Date.now() - this.startTime,
|
|
175
195
|
contextSaves: this.contextService.getState().saveCount,
|
|
176
|
-
linearSyncs: this.linearService.getState().syncCount
|
|
196
|
+
linearSyncs: this.linearService.getState().syncCount,
|
|
197
|
+
maintenanceRuns: this.maintenanceService.getState().ftsRebuilds
|
|
177
198
|
});
|
|
178
199
|
if (this.heartbeatInterval) {
|
|
179
200
|
clearInterval(this.heartbeatInterval);
|
|
@@ -181,6 +202,7 @@ class UnifiedDaemon {
|
|
|
181
202
|
}
|
|
182
203
|
this.contextService.stop();
|
|
183
204
|
this.linearService.stop();
|
|
205
|
+
this.maintenanceService.stop();
|
|
184
206
|
this.cleanup();
|
|
185
207
|
const exitCode = reason === "sigterm" || reason === "sigint" || reason === "sighup" ? 0 : 1;
|
|
186
208
|
process.exit(exitCode);
|
|
@@ -198,17 +220,20 @@ class UnifiedDaemon {
|
|
|
198
220
|
config: {
|
|
199
221
|
context: this.config.context.enabled,
|
|
200
222
|
linear: this.config.linear.enabled,
|
|
223
|
+
maintenance: this.config.maintenance.enabled,
|
|
201
224
|
fileWatch: this.config.fileWatch.enabled
|
|
202
225
|
}
|
|
203
226
|
});
|
|
204
227
|
this.contextService.start();
|
|
205
228
|
await this.linearService.start();
|
|
229
|
+
this.maintenanceService.start();
|
|
206
230
|
this.heartbeatInterval = setInterval(() => {
|
|
207
231
|
this.updateStatus();
|
|
208
232
|
}, this.config.heartbeatInterval * 1e3);
|
|
209
233
|
this.updateStatus();
|
|
210
234
|
}
|
|
211
235
|
getStatus() {
|
|
236
|
+
const maintenanceState = this.maintenanceService.getState();
|
|
212
237
|
return {
|
|
213
238
|
running: !this.isShuttingDown,
|
|
214
239
|
pid: process.pid,
|
|
@@ -225,13 +250,21 @@ class UnifiedDaemon {
|
|
|
225
250
|
lastRun: this.linearService.getState().lastSyncTime || void 0,
|
|
226
251
|
syncCount: this.linearService.getState().syncCount
|
|
227
252
|
},
|
|
253
|
+
maintenance: {
|
|
254
|
+
enabled: this.config.maintenance.enabled,
|
|
255
|
+
lastRun: maintenanceState.lastRunTime || void 0,
|
|
256
|
+
staleFramesCleaned: maintenanceState.staleFramesCleaned,
|
|
257
|
+
ftsRebuilds: maintenanceState.ftsRebuilds,
|
|
258
|
+
embeddingsGenerated: maintenanceState.embeddingsGenerated
|
|
259
|
+
},
|
|
228
260
|
fileWatch: {
|
|
229
261
|
enabled: this.config.fileWatch.enabled
|
|
230
262
|
}
|
|
231
263
|
},
|
|
232
264
|
errors: [
|
|
233
265
|
...this.contextService.getState().errors,
|
|
234
|
-
...this.linearService.getState().errors
|
|
266
|
+
...this.linearService.getState().errors,
|
|
267
|
+
...maintenanceState.errors
|
|
235
268
|
]
|
|
236
269
|
};
|
|
237
270
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/daemon/unified-daemon.ts"],
|
|
4
|
-
"sourcesContent": ["#!/usr/bin/env node\n\n/**\n * Unified Daemon for StackMemory\n *\n * Single background process managing multiple services:\n * - Context auto-save\n * - Linear sync\n * - File watch (future)\n */\n\nimport {\n existsSync,\n writeFileSync,\n unlinkSync,\n appendFileSync,\n readFileSync,\n} from 'fs';\nimport {\n loadDaemonConfig,\n getDaemonPaths,\n writeDaemonStatus,\n type DaemonConfig,\n type DaemonStatus,\n} from './daemon-config.js';\nimport { DaemonContextService } from './services/context-service.js';\nimport { DaemonLinearService } from './services/linear-service.js';\n\ninterface LogEntry {\n timestamp: string;\n level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';\n service: string;\n message: string;\n data?: unknown;\n}\n\nexport class UnifiedDaemon {\n private config: DaemonConfig;\n private paths: ReturnType<typeof getDaemonPaths>;\n private contextService: DaemonContextService;\n private linearService: DaemonLinearService;\n private heartbeatInterval?: NodeJS.Timeout;\n private isShuttingDown = false;\n private startTime: number = 0;\n\n constructor(config?: Partial<DaemonConfig>) {\n this.paths = getDaemonPaths();\n this.config = { ...loadDaemonConfig(), ...config };\n\n // Initialize services\n this.contextService = new DaemonContextService(\n this.config.context,\n (level, msg, data) => this.log(level, 'context', msg, data)\n );\n\n this.linearService = new DaemonLinearService(\n this.config.linear,\n (level, msg, data) => this.log(level, 'linear', msg, data)\n );\n }\n\n private log(\n level: string,\n service: string,\n message: string,\n data?: unknown\n ): void {\n const logLevel = level.toUpperCase() as LogEntry['level'];\n\n // Check log level\n const levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];\n const configLevel = this.config.logLevel.toUpperCase();\n if (levels.indexOf(logLevel) < levels.indexOf(configLevel)) {\n return;\n }\n\n const entry: LogEntry = {\n timestamp: new Date().toISOString(),\n level: logLevel,\n service,\n message,\n data,\n };\n\n const logLine = JSON.stringify(entry) + '\\n';\n\n try {\n appendFileSync(this.paths.logFile, logLine);\n } catch {\n console.error(`[${entry.timestamp}] ${level} [${service}]: ${message}`);\n }\n }\n\n private checkIdempotency(): boolean {\n if (existsSync(this.paths.pidFile)) {\n try {\n const existingPid = readFileSync(this.paths.pidFile, 'utf8').trim();\n const pid = parseInt(existingPid, 10);\n\n // Check if process is running\n try {\n process.kill(pid, 0);\n this.log('WARN', 'daemon', 'Daemon already running', { pid });\n return false;\n } catch {\n // Process not running, stale PID\n this.log('INFO', 'daemon', 'Cleaning stale PID file', { pid });\n unlinkSync(this.paths.pidFile);\n }\n } catch {\n try {\n unlinkSync(this.paths.pidFile);\n } catch {\n // Ignore\n }\n }\n }\n return true;\n }\n\n private writePidFile(): void {\n writeFileSync(this.paths.pidFile, process.pid.toString());\n this.log('INFO', 'daemon', 'PID file created', {\n pid: process.pid,\n file: this.paths.pidFile,\n });\n }\n\n private updateStatus(): void {\n const status: DaemonStatus = {\n running: true,\n pid: process.pid,\n startTime: this.startTime,\n uptime: Date.now() - this.startTime,\n services: {\n context: {\n enabled: this.config.context.enabled,\n lastRun: this.contextService.getState().lastSaveTime || undefined,\n saveCount: this.contextService.getState().saveCount,\n },\n linear: {\n enabled: this.config.linear.enabled,\n lastRun: this.linearService.getState().lastSyncTime || undefined,\n syncCount: this.linearService.getState().syncCount,\n },\n fileWatch: {\n enabled: this.config.fileWatch.enabled,\n },\n },\n errors: [\n ...this.contextService.getState().errors.slice(-5),\n ...this.linearService.getState().errors.slice(-5),\n ],\n };\n\n writeDaemonStatus(status);\n }\n\n private setupSignalHandlers(): void {\n const handleSignal = (signal: string) => {\n this.log('INFO', 'daemon', `Received ${signal}, shutting down`);\n this.shutdown(signal.toLowerCase());\n };\n\n process.on('SIGTERM', () => handleSignal('SIGTERM'));\n process.on('SIGINT', () => handleSignal('SIGINT'));\n process.on('SIGHUP', () => handleSignal('SIGHUP'));\n\n process.on('uncaughtException', (err) => {\n this.log('ERROR', 'daemon', 'Uncaught exception', {\n error: err.message,\n stack: err.stack,\n });\n this.shutdown('uncaught_exception');\n });\n\n process.on('unhandledRejection', (reason) => {\n this.log('ERROR', 'daemon', 'Unhandled rejection', {\n reason: String(reason),\n });\n });\n }\n\n private cleanup(): void {\n // Remove PID file\n try {\n if (existsSync(this.paths.pidFile)) {\n unlinkSync(this.paths.pidFile);\n this.log('INFO', 'daemon', 'PID file removed');\n }\n } catch (e) {\n this.log('WARN', 'daemon', 'Failed to remove PID file', {\n error: String(e),\n });\n }\n\n // Update status\n const finalStatus: DaemonStatus = {\n running: false,\n startTime: this.startTime,\n uptime: Date.now() - this.startTime,\n services: {\n context: {\n enabled: false,\n saveCount: this.contextService.getState().saveCount,\n },\n linear: {\n enabled: false,\n syncCount: this.linearService.getState().syncCount,\n },\n fileWatch: { enabled: false },\n },\n errors: [],\n };\n writeDaemonStatus(finalStatus);\n }\n\n private shutdown(reason: string): void {\n if (this.isShuttingDown) return;\n this.isShuttingDown = true;\n\n this.log('INFO', 'daemon', 'Daemon shutting down', {\n reason,\n uptime: Date.now() - this.startTime,\n contextSaves: this.contextService.getState().saveCount,\n linearSyncs: this.linearService.getState().syncCount,\n });\n\n // Stop heartbeat\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = undefined;\n }\n\n // Stop services\n this.contextService.stop();\n this.linearService.stop();\n\n // Cleanup\n this.cleanup();\n\n const exitCode =\n reason === 'sigterm' || reason === 'sigint' || reason === 'sighup'\n ? 0\n : 1;\n process.exit(exitCode);\n }\n\n async start(): Promise<void> {\n // Check idempotency\n if (!this.checkIdempotency()) {\n this.log('INFO', 'daemon', 'Exiting - daemon already running');\n process.exit(0);\n }\n\n this.startTime = Date.now();\n\n // Write PID file\n this.writePidFile();\n\n // Setup signal handlers\n this.setupSignalHandlers();\n\n this.log('INFO', 'daemon', 'Unified daemon started', {\n pid: process.pid,\n config: {\n context: this.config.context.enabled,\n linear: this.config.linear.enabled,\n fileWatch: this.config.fileWatch.enabled,\n },\n });\n\n // Start services\n this.contextService.start();\n await this.linearService.start();\n\n // Start heartbeat\n this.heartbeatInterval = setInterval(() => {\n this.updateStatus();\n }, this.config.heartbeatInterval * 1000);\n\n // Initial status update\n this.updateStatus();\n }\n\n getStatus(): DaemonStatus {\n return {\n running: !this.isShuttingDown,\n pid: process.pid,\n startTime: this.startTime,\n uptime: Date.now() - this.startTime,\n services: {\n context: {\n enabled: this.config.context.enabled,\n lastRun: this.contextService.getState().lastSaveTime || undefined,\n saveCount: this.contextService.getState().saveCount,\n },\n linear: {\n enabled: this.config.linear.enabled,\n lastRun: this.linearService.getState().lastSyncTime || undefined,\n syncCount: this.linearService.getState().syncCount,\n },\n fileWatch: {\n enabled: this.config.fileWatch.enabled,\n },\n },\n errors: [\n ...this.contextService.getState().errors,\n ...this.linearService.getState().errors,\n ],\n };\n }\n}\n\n// CLI entry point\nif (\n import.meta.url === `file://${process.argv[1]}` ||\n process.argv[1]?.endsWith('unified-daemon.js')\n) {\n const args = process.argv.slice(2);\n const config: Partial<DaemonConfig> = {};\n\n // Parse command line args\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === '--save-interval' && args[i + 1]) {\n config.context = { enabled: true, interval: parseInt(args[i + 1], 10) };\n i++;\n } else if (arg === '--linear-interval' && args[i + 1]) {\n config.linear = {\n enabled: true,\n interval: parseInt(args[i + 1], 10),\n retryAttempts: 3,\n retryDelay: 30000,\n };\n i++;\n } else if (arg === '--no-linear') {\n config.linear = {\n enabled: false,\n interval: 60,\n retryAttempts: 3,\n retryDelay: 30000,\n };\n } else if (arg === '--log-level' && args[i + 1]) {\n config.logLevel = args[i + 1] as DaemonConfig['logLevel'];\n i++;\n }\n }\n\n const daemon = new UnifiedDaemon(config);\n daemon.start().catch((err) => {\n console.error('Failed to start daemon:', err);\n process.exit(1);\n });\n}\n"],
|
|
5
|
-
"mappings": ";;;;;AAWA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n\n/**\n * Unified Daemon for StackMemory\n *\n * Single background process managing multiple services:\n * - Context auto-save\n * - Linear sync\n * - File watch (future)\n */\n\nimport {\n existsSync,\n writeFileSync,\n unlinkSync,\n appendFileSync,\n readFileSync,\n} from 'fs';\nimport {\n loadDaemonConfig,\n getDaemonPaths,\n writeDaemonStatus,\n type DaemonConfig,\n type DaemonStatus,\n} from './daemon-config.js';\nimport { DaemonContextService } from './services/context-service.js';\nimport { DaemonLinearService } from './services/linear-service.js';\nimport { DaemonMaintenanceService } from './services/maintenance-service.js';\n\ninterface LogEntry {\n timestamp: string;\n level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';\n service: string;\n message: string;\n data?: unknown;\n}\n\nexport class UnifiedDaemon {\n private config: DaemonConfig;\n private paths: ReturnType<typeof getDaemonPaths>;\n private contextService: DaemonContextService;\n private linearService: DaemonLinearService;\n private maintenanceService: DaemonMaintenanceService;\n private heartbeatInterval?: NodeJS.Timeout;\n private isShuttingDown = false;\n private startTime: number = 0;\n\n constructor(config?: Partial<DaemonConfig>) {\n this.paths = getDaemonPaths();\n this.config = { ...loadDaemonConfig(), ...config };\n\n // Initialize services\n this.contextService = new DaemonContextService(\n this.config.context,\n (level, msg, data) => this.log(level, 'context', msg, data)\n );\n\n this.linearService = new DaemonLinearService(\n this.config.linear,\n (level, msg, data) => this.log(level, 'linear', msg, data)\n );\n\n this.maintenanceService = new DaemonMaintenanceService(\n this.config.maintenance,\n (level, msg, data) => this.log(level, 'maintenance', msg, data)\n );\n }\n\n private log(\n level: string,\n service: string,\n message: string,\n data?: unknown\n ): void {\n const logLevel = level.toUpperCase() as LogEntry['level'];\n\n // Check log level\n const levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];\n const configLevel = this.config.logLevel.toUpperCase();\n if (levels.indexOf(logLevel) < levels.indexOf(configLevel)) {\n return;\n }\n\n const entry: LogEntry = {\n timestamp: new Date().toISOString(),\n level: logLevel,\n service,\n message,\n data,\n };\n\n const logLine = JSON.stringify(entry) + '\\n';\n\n try {\n appendFileSync(this.paths.logFile, logLine);\n } catch {\n console.error(`[${entry.timestamp}] ${level} [${service}]: ${message}`);\n }\n }\n\n private checkIdempotency(): boolean {\n if (existsSync(this.paths.pidFile)) {\n try {\n const existingPid = readFileSync(this.paths.pidFile, 'utf8').trim();\n const pid = parseInt(existingPid, 10);\n\n // Check if process is running\n try {\n process.kill(pid, 0);\n this.log('WARN', 'daemon', 'Daemon already running', { pid });\n return false;\n } catch {\n // Process not running, stale PID\n this.log('INFO', 'daemon', 'Cleaning stale PID file', { pid });\n unlinkSync(this.paths.pidFile);\n }\n } catch {\n try {\n unlinkSync(this.paths.pidFile);\n } catch {\n // Ignore\n }\n }\n }\n return true;\n }\n\n private writePidFile(): void {\n writeFileSync(this.paths.pidFile, process.pid.toString());\n this.log('INFO', 'daemon', 'PID file created', {\n pid: process.pid,\n file: this.paths.pidFile,\n });\n }\n\n private updateStatus(): void {\n const maintenanceState = this.maintenanceService.getState();\n const status: DaemonStatus = {\n running: true,\n pid: process.pid,\n startTime: this.startTime,\n uptime: Date.now() - this.startTime,\n services: {\n context: {\n enabled: this.config.context.enabled,\n lastRun: this.contextService.getState().lastSaveTime || undefined,\n saveCount: this.contextService.getState().saveCount,\n },\n linear: {\n enabled: this.config.linear.enabled,\n lastRun: this.linearService.getState().lastSyncTime || undefined,\n syncCount: this.linearService.getState().syncCount,\n },\n maintenance: {\n enabled: this.config.maintenance.enabled,\n lastRun: maintenanceState.lastRunTime || undefined,\n staleFramesCleaned: maintenanceState.staleFramesCleaned,\n ftsRebuilds: maintenanceState.ftsRebuilds,\n embeddingsGenerated: maintenanceState.embeddingsGenerated,\n },\n fileWatch: {\n enabled: this.config.fileWatch.enabled,\n },\n },\n errors: [\n ...this.contextService.getState().errors.slice(-5),\n ...this.linearService.getState().errors.slice(-5),\n ...maintenanceState.errors.slice(-5),\n ],\n };\n\n writeDaemonStatus(status);\n }\n\n private setupSignalHandlers(): void {\n const handleSignal = (signal: string) => {\n this.log('INFO', 'daemon', `Received ${signal}, shutting down`);\n this.shutdown(signal.toLowerCase());\n };\n\n process.on('SIGTERM', () => handleSignal('SIGTERM'));\n process.on('SIGINT', () => handleSignal('SIGINT'));\n process.on('SIGHUP', () => handleSignal('SIGHUP'));\n\n process.on('uncaughtException', (err) => {\n this.log('ERROR', 'daemon', 'Uncaught exception', {\n error: err.message,\n stack: err.stack,\n });\n this.shutdown('uncaught_exception');\n });\n\n process.on('unhandledRejection', (reason) => {\n this.log('ERROR', 'daemon', 'Unhandled rejection', {\n reason: String(reason),\n });\n });\n }\n\n private cleanup(): void {\n // Remove PID file\n try {\n if (existsSync(this.paths.pidFile)) {\n unlinkSync(this.paths.pidFile);\n this.log('INFO', 'daemon', 'PID file removed');\n }\n } catch (e) {\n this.log('WARN', 'daemon', 'Failed to remove PID file', {\n error: String(e),\n });\n }\n\n // Update status\n const finalStatus: DaemonStatus = {\n running: false,\n startTime: this.startTime,\n uptime: Date.now() - this.startTime,\n services: {\n context: {\n enabled: false,\n saveCount: this.contextService.getState().saveCount,\n },\n linear: {\n enabled: false,\n syncCount: this.linearService.getState().syncCount,\n },\n maintenance: {\n enabled: false,\n staleFramesCleaned:\n this.maintenanceService.getState().staleFramesCleaned,\n ftsRebuilds: this.maintenanceService.getState().ftsRebuilds,\n },\n fileWatch: { enabled: false },\n },\n errors: [],\n };\n writeDaemonStatus(finalStatus);\n }\n\n private shutdown(reason: string): void {\n if (this.isShuttingDown) return;\n this.isShuttingDown = true;\n\n this.log('INFO', 'daemon', 'Daemon shutting down', {\n reason,\n uptime: Date.now() - this.startTime,\n contextSaves: this.contextService.getState().saveCount,\n linearSyncs: this.linearService.getState().syncCount,\n maintenanceRuns: this.maintenanceService.getState().ftsRebuilds,\n });\n\n // Stop heartbeat\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = undefined;\n }\n\n // Stop services\n this.contextService.stop();\n this.linearService.stop();\n this.maintenanceService.stop();\n\n // Cleanup\n this.cleanup();\n\n const exitCode =\n reason === 'sigterm' || reason === 'sigint' || reason === 'sighup'\n ? 0\n : 1;\n process.exit(exitCode);\n }\n\n async start(): Promise<void> {\n // Check idempotency\n if (!this.checkIdempotency()) {\n this.log('INFO', 'daemon', 'Exiting - daemon already running');\n process.exit(0);\n }\n\n this.startTime = Date.now();\n\n // Write PID file\n this.writePidFile();\n\n // Setup signal handlers\n this.setupSignalHandlers();\n\n this.log('INFO', 'daemon', 'Unified daemon started', {\n pid: process.pid,\n config: {\n context: this.config.context.enabled,\n linear: this.config.linear.enabled,\n maintenance: this.config.maintenance.enabled,\n fileWatch: this.config.fileWatch.enabled,\n },\n });\n\n // Start services\n this.contextService.start();\n await this.linearService.start();\n this.maintenanceService.start();\n\n // Start heartbeat\n this.heartbeatInterval = setInterval(() => {\n this.updateStatus();\n }, this.config.heartbeatInterval * 1000);\n\n // Initial status update\n this.updateStatus();\n }\n\n getStatus(): DaemonStatus {\n const maintenanceState = this.maintenanceService.getState();\n return {\n running: !this.isShuttingDown,\n pid: process.pid,\n startTime: this.startTime,\n uptime: Date.now() - this.startTime,\n services: {\n context: {\n enabled: this.config.context.enabled,\n lastRun: this.contextService.getState().lastSaveTime || undefined,\n saveCount: this.contextService.getState().saveCount,\n },\n linear: {\n enabled: this.config.linear.enabled,\n lastRun: this.linearService.getState().lastSyncTime || undefined,\n syncCount: this.linearService.getState().syncCount,\n },\n maintenance: {\n enabled: this.config.maintenance.enabled,\n lastRun: maintenanceState.lastRunTime || undefined,\n staleFramesCleaned: maintenanceState.staleFramesCleaned,\n ftsRebuilds: maintenanceState.ftsRebuilds,\n embeddingsGenerated: maintenanceState.embeddingsGenerated,\n },\n fileWatch: {\n enabled: this.config.fileWatch.enabled,\n },\n },\n errors: [\n ...this.contextService.getState().errors,\n ...this.linearService.getState().errors,\n ...maintenanceState.errors,\n ],\n };\n }\n}\n\n// CLI entry point\nif (\n import.meta.url === `file://${process.argv[1]}` ||\n process.argv[1]?.endsWith('unified-daemon.js')\n) {\n const args = process.argv.slice(2);\n const config: Partial<DaemonConfig> = {};\n\n // Parse command line args\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === '--save-interval' && args[i + 1]) {\n config.context = { enabled: true, interval: parseInt(args[i + 1], 10) };\n i++;\n } else if (arg === '--linear-interval' && args[i + 1]) {\n config.linear = {\n enabled: true,\n interval: parseInt(args[i + 1], 10),\n retryAttempts: 3,\n retryDelay: 30000,\n };\n i++;\n } else if (arg === '--no-linear') {\n config.linear = {\n enabled: false,\n interval: 60,\n retryAttempts: 3,\n retryDelay: 30000,\n };\n } else if (arg === '--log-level' && args[i + 1]) {\n config.logLevel = args[i + 1] as DaemonConfig['logLevel'];\n i++;\n }\n }\n\n const daemon = new UnifiedDaemon(config);\n daemon.start().catch((err) => {\n console.error('Failed to start daemon:', err);\n process.exit(1);\n });\n}\n"],
|
|
5
|
+
"mappings": ";;;;;AAWA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AACpC,SAAS,gCAAgC;AAUlC,MAAM,cAAc;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,YAAoB;AAAA,EAE5B,YAAY,QAAgC;AAC1C,SAAK,QAAQ,eAAe;AAC5B,SAAK,SAAS,EAAE,GAAG,iBAAiB,GAAG,GAAG,OAAO;AAGjD,SAAK,iBAAiB,IAAI;AAAA,MACxB,KAAK,OAAO;AAAA,MACZ,CAAC,OAAO,KAAK,SAAS,KAAK,IAAI,OAAO,WAAW,KAAK,IAAI;AAAA,IAC5D;AAEA,SAAK,gBAAgB,IAAI;AAAA,MACvB,KAAK,OAAO;AAAA,MACZ,CAAC,OAAO,KAAK,SAAS,KAAK,IAAI,OAAO,UAAU,KAAK,IAAI;AAAA,IAC3D;AAEA,SAAK,qBAAqB,IAAI;AAAA,MAC5B,KAAK,OAAO;AAAA,MACZ,CAAC,OAAO,KAAK,SAAS,KAAK,IAAI,OAAO,eAAe,KAAK,IAAI;AAAA,IAChE;AAAA,EACF;AAAA,EAEQ,IACN,OACA,SACA,SACA,MACM;AACN,UAAM,WAAW,MAAM,YAAY;AAGnC,UAAM,SAAS,CAAC,SAAS,QAAQ,QAAQ,OAAO;AAChD,UAAM,cAAc,KAAK,OAAO,SAAS,YAAY;AACrD,QAAI,OAAO,QAAQ,QAAQ,IAAI,OAAO,QAAQ,WAAW,GAAG;AAC1D;AAAA,IACF;AAEA,UAAM,QAAkB;AAAA,MACtB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,OAAO;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,UAAM,UAAU,KAAK,UAAU,KAAK,IAAI;AAExC,QAAI;AACF,qBAAe,KAAK,MAAM,SAAS,OAAO;AAAA,IAC5C,QAAQ;AACN,cAAQ,MAAM,IAAI,MAAM,SAAS,KAAK,KAAK,KAAK,OAAO,MAAM,OAAO,EAAE;AAAA,IACxE;AAAA,EACF;AAAA,EAEQ,mBAA4B;AAClC,QAAI,WAAW,KAAK,MAAM,OAAO,GAAG;AAClC,UAAI;AACF,cAAM,cAAc,aAAa,KAAK,MAAM,SAAS,MAAM,EAAE,KAAK;AAClE,cAAM,MAAM,SAAS,aAAa,EAAE;AAGpC,YAAI;AACF,kBAAQ,KAAK,KAAK,CAAC;AACnB,eAAK,IAAI,QAAQ,UAAU,0BAA0B,EAAE,IAAI,CAAC;AAC5D,iBAAO;AAAA,QACT,QAAQ;AAEN,eAAK,IAAI,QAAQ,UAAU,2BAA2B,EAAE,IAAI,CAAC;AAC7D,qBAAW,KAAK,MAAM,OAAO;AAAA,QAC/B;AAAA,MACF,QAAQ;AACN,YAAI;AACF,qBAAW,KAAK,MAAM,OAAO;AAAA,QAC/B,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,eAAqB;AAC3B,kBAAc,KAAK,MAAM,SAAS,QAAQ,IAAI,SAAS,CAAC;AACxD,SAAK,IAAI,QAAQ,UAAU,oBAAoB;AAAA,MAC7C,KAAK,QAAQ;AAAA,MACb,MAAM,KAAK,MAAM;AAAA,IACnB,CAAC;AAAA,EACH;AAAA,EAEQ,eAAqB;AAC3B,UAAM,mBAAmB,KAAK,mBAAmB,SAAS;AAC1D,UAAM,SAAuB;AAAA,MAC3B,SAAS;AAAA,MACT,KAAK,QAAQ;AAAA,MACb,WAAW,KAAK;AAAA,MAChB,QAAQ,KAAK,IAAI,IAAI,KAAK;AAAA,MAC1B,UAAU;AAAA,QACR,SAAS;AAAA,UACP,SAAS,KAAK,OAAO,QAAQ;AAAA,UAC7B,SAAS,KAAK,eAAe,SAAS,EAAE,gBAAgB;AAAA,UACxD,WAAW,KAAK,eAAe,SAAS,EAAE;AAAA,QAC5C;AAAA,QACA,QAAQ;AAAA,UACN,SAAS,KAAK,OAAO,OAAO;AAAA,UAC5B,SAAS,KAAK,cAAc,SAAS,EAAE,gBAAgB;AAAA,UACvD,WAAW,KAAK,cAAc,SAAS,EAAE;AAAA,QAC3C;AAAA,QACA,aAAa;AAAA,UACX,SAAS,KAAK,OAAO,YAAY;AAAA,UACjC,SAAS,iBAAiB,eAAe;AAAA,UACzC,oBAAoB,iBAAiB;AAAA,UACrC,aAAa,iBAAiB;AAAA,UAC9B,qBAAqB,iBAAiB;AAAA,QACxC;AAAA,QACA,WAAW;AAAA,UACT,SAAS,KAAK,OAAO,UAAU;AAAA,QACjC;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,GAAG,KAAK,eAAe,SAAS,EAAE,OAAO,MAAM,EAAE;AAAA,QACjD,GAAG,KAAK,cAAc,SAAS,EAAE,OAAO,MAAM,EAAE;AAAA,QAChD,GAAG,iBAAiB,OAAO,MAAM,EAAE;AAAA,MACrC;AAAA,IACF;AAEA,sBAAkB,MAAM;AAAA,EAC1B;AAAA,EAEQ,sBAA4B;AAClC,UAAM,eAAe,CAAC,WAAmB;AACvC,WAAK,IAAI,QAAQ,UAAU,YAAY,MAAM,iBAAiB;AAC9D,WAAK,SAAS,OAAO,YAAY,CAAC;AAAA,IACpC;AAEA,YAAQ,GAAG,WAAW,MAAM,aAAa,SAAS,CAAC;AACnD,YAAQ,GAAG,UAAU,MAAM,aAAa,QAAQ,CAAC;AACjD,YAAQ,GAAG,UAAU,MAAM,aAAa,QAAQ,CAAC;AAEjD,YAAQ,GAAG,qBAAqB,CAAC,QAAQ;AACvC,WAAK,IAAI,SAAS,UAAU,sBAAsB;AAAA,QAChD,OAAO,IAAI;AAAA,QACX,OAAO,IAAI;AAAA,MACb,CAAC;AACD,WAAK,SAAS,oBAAoB;AAAA,IACpC,CAAC;AAED,YAAQ,GAAG,sBAAsB,CAAC,WAAW;AAC3C,WAAK,IAAI,SAAS,UAAU,uBAAuB;AAAA,QACjD,QAAQ,OAAO,MAAM;AAAA,MACvB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEQ,UAAgB;AAEtB,QAAI;AACF,UAAI,WAAW,KAAK,MAAM,OAAO,GAAG;AAClC,mBAAW,KAAK,MAAM,OAAO;AAC7B,aAAK,IAAI,QAAQ,UAAU,kBAAkB;AAAA,MAC/C;AAAA,IACF,SAAS,GAAG;AACV,WAAK,IAAI,QAAQ,UAAU,6BAA6B;AAAA,QACtD,OAAO,OAAO,CAAC;AAAA,MACjB,CAAC;AAAA,IACH;AAGA,UAAM,cAA4B;AAAA,MAChC,SAAS;AAAA,MACT,WAAW,KAAK;AAAA,MAChB,QAAQ,KAAK,IAAI,IAAI,KAAK;AAAA,MAC1B,UAAU;AAAA,QACR,SAAS;AAAA,UACP,SAAS;AAAA,UACT,WAAW,KAAK,eAAe,SAAS,EAAE;AAAA,QAC5C;AAAA,QACA,QAAQ;AAAA,UACN,SAAS;AAAA,UACT,WAAW,KAAK,cAAc,SAAS,EAAE;AAAA,QAC3C;AAAA,QACA,aAAa;AAAA,UACX,SAAS;AAAA,UACT,oBACE,KAAK,mBAAmB,SAAS,EAAE;AAAA,UACrC,aAAa,KAAK,mBAAmB,SAAS,EAAE;AAAA,QAClD;AAAA,QACA,WAAW,EAAE,SAAS,MAAM;AAAA,MAC9B;AAAA,MACA,QAAQ,CAAC;AAAA,IACX;AACA,sBAAkB,WAAW;AAAA,EAC/B;AAAA,EAEQ,SAAS,QAAsB;AACrC,QAAI,KAAK,eAAgB;AACzB,SAAK,iBAAiB;AAEtB,SAAK,IAAI,QAAQ,UAAU,wBAAwB;AAAA,MACjD;AAAA,MACA,QAAQ,KAAK,IAAI,IAAI,KAAK;AAAA,MAC1B,cAAc,KAAK,eAAe,SAAS,EAAE;AAAA,MAC7C,aAAa,KAAK,cAAc,SAAS,EAAE;AAAA,MAC3C,iBAAiB,KAAK,mBAAmB,SAAS,EAAE;AAAA,IACtD,CAAC;AAGD,QAAI,KAAK,mBAAmB;AAC1B,oBAAc,KAAK,iBAAiB;AACpC,WAAK,oBAAoB;AAAA,IAC3B;AAGA,SAAK,eAAe,KAAK;AACzB,SAAK,cAAc,KAAK;AACxB,SAAK,mBAAmB,KAAK;AAG7B,SAAK,QAAQ;AAEb,UAAM,WACJ,WAAW,aAAa,WAAW,YAAY,WAAW,WACtD,IACA;AACN,YAAQ,KAAK,QAAQ;AAAA,EACvB;AAAA,EAEA,MAAM,QAAuB;AAE3B,QAAI,CAAC,KAAK,iBAAiB,GAAG;AAC5B,WAAK,IAAI,QAAQ,UAAU,kCAAkC;AAC7D,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,SAAK,YAAY,KAAK,IAAI;AAG1B,SAAK,aAAa;AAGlB,SAAK,oBAAoB;AAEzB,SAAK,IAAI,QAAQ,UAAU,0BAA0B;AAAA,MACnD,KAAK,QAAQ;AAAA,MACb,QAAQ;AAAA,QACN,SAAS,KAAK,OAAO,QAAQ;AAAA,QAC7B,QAAQ,KAAK,OAAO,OAAO;AAAA,QAC3B,aAAa,KAAK,OAAO,YAAY;AAAA,QACrC,WAAW,KAAK,OAAO,UAAU;AAAA,MACnC;AAAA,IACF,CAAC;AAGD,SAAK,eAAe,MAAM;AAC1B,UAAM,KAAK,cAAc,MAAM;AAC/B,SAAK,mBAAmB,MAAM;AAG9B,SAAK,oBAAoB,YAAY,MAAM;AACzC,WAAK,aAAa;AAAA,IACpB,GAAG,KAAK,OAAO,oBAAoB,GAAI;AAGvC,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,YAA0B;AACxB,UAAM,mBAAmB,KAAK,mBAAmB,SAAS;AAC1D,WAAO;AAAA,MACL,SAAS,CAAC,KAAK;AAAA,MACf,KAAK,QAAQ;AAAA,MACb,WAAW,KAAK;AAAA,MAChB,QAAQ,KAAK,IAAI,IAAI,KAAK;AAAA,MAC1B,UAAU;AAAA,QACR,SAAS;AAAA,UACP,SAAS,KAAK,OAAO,QAAQ;AAAA,UAC7B,SAAS,KAAK,eAAe,SAAS,EAAE,gBAAgB;AAAA,UACxD,WAAW,KAAK,eAAe,SAAS,EAAE;AAAA,QAC5C;AAAA,QACA,QAAQ;AAAA,UACN,SAAS,KAAK,OAAO,OAAO;AAAA,UAC5B,SAAS,KAAK,cAAc,SAAS,EAAE,gBAAgB;AAAA,UACvD,WAAW,KAAK,cAAc,SAAS,EAAE;AAAA,QAC3C;AAAA,QACA,aAAa;AAAA,UACX,SAAS,KAAK,OAAO,YAAY;AAAA,UACjC,SAAS,iBAAiB,eAAe;AAAA,UACzC,oBAAoB,iBAAiB;AAAA,UACrC,aAAa,iBAAiB;AAAA,UAC9B,qBAAqB,iBAAiB;AAAA,QACxC;AAAA,QACA,WAAW;AAAA,UACT,SAAS,KAAK,OAAO,UAAU;AAAA,QACjC;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,GAAG,KAAK,eAAe,SAAS,EAAE;AAAA,QAClC,GAAG,KAAK,cAAc,SAAS,EAAE;AAAA,QACjC,GAAG,iBAAiB;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AACF;AAGA,IACE,YAAY,QAAQ,UAAU,QAAQ,KAAK,CAAC,CAAC,MAC7C,QAAQ,KAAK,CAAC,GAAG,SAAS,mBAAmB,GAC7C;AACA,QAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,QAAM,SAAgC,CAAC;AAGvC,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,MAAM,KAAK,CAAC;AAClB,QAAI,QAAQ,qBAAqB,KAAK,IAAI,CAAC,GAAG;AAC5C,aAAO,UAAU,EAAE,SAAS,MAAM,UAAU,SAAS,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE;AACtE;AAAA,IACF,WAAW,QAAQ,uBAAuB,KAAK,IAAI,CAAC,GAAG;AACrD,aAAO,SAAS;AAAA,QACd,SAAS;AAAA,QACT,UAAU,SAAS,KAAK,IAAI,CAAC,GAAG,EAAE;AAAA,QAClC,eAAe;AAAA,QACf,YAAY;AAAA,MACd;AACA;AAAA,IACF,WAAW,QAAQ,eAAe;AAChC,aAAO,SAAS;AAAA,QACd,SAAS;AAAA,QACT,UAAU;AAAA,QACV,eAAe;AAAA,QACf,YAAY;AAAA,MACd;AAAA,IACF,WAAW,QAAQ,iBAAiB,KAAK,IAAI,CAAC,GAAG;AAC/C,aAAO,WAAW,KAAK,IAAI,CAAC;AAC5B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,IAAI,cAAc,MAAM;AACvC,SAAO,MAAM,EAAE,MAAM,CAAC,QAAQ;AAC5B,YAAQ,MAAM,2BAA2B,GAAG;AAC5C,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"@types/blessed": "^0.1.27",
|
|
110
110
|
"@types/inquirer": "^9.0.9",
|
|
111
111
|
"@types/pg": "^8.16.0",
|
|
112
|
-
"better-sqlite3": "^9.
|
|
112
|
+
"better-sqlite3": "^9.6.0",
|
|
113
113
|
"chalk": "^5.3.0",
|
|
114
114
|
"cli-table3": "^0.6.5",
|
|
115
115
|
"commander": "^11.1.0",
|
|
@@ -35,16 +35,16 @@ const distDir = join(__dirname, '..', 'dist');
|
|
|
35
35
|
* Returns true if user consents, false otherwise
|
|
36
36
|
*/
|
|
37
37
|
async function askForConsent() {
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
)
|
|
47
|
-
|
|
38
|
+
// Auto-install if STACKMEMORY_AUTO_HOOKS=true (no prompt needed)
|
|
39
|
+
if (process.env.STACKMEMORY_AUTO_HOOKS === 'true') {
|
|
40
|
+
console.log(
|
|
41
|
+
'STACKMEMORY_AUTO_HOOKS=true — installing hooks automatically.'
|
|
42
|
+
);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Skip prompt if not interactive (CI/CD, piped input)
|
|
47
|
+
if (!process.stdin.isTTY || process.env.CI === 'true') {
|
|
48
48
|
console.log(
|
|
49
49
|
'StackMemory installed. Run "stackmemory setup-mcp" to configure Claude Code integration.'
|
|
50
50
|
);
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* 3. Logs to ~/.stackmemory/logs/hook-errors.log on failure (non-blocking)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
15
|
|
|
16
16
|
async function onTaskComplete() {
|
|
17
17
|
try {
|
|
@@ -20,14 +20,13 @@ async function onTaskComplete() {
|
|
|
20
20
|
// Sync Linear if STA task
|
|
21
21
|
if (input.task && input.task.includes('STA-')) {
|
|
22
22
|
try {
|
|
23
|
-
const smBin = path.join(process.env.HOME || '', '.stackmemory', 'bin');
|
|
24
23
|
const syncScript = path.join(
|
|
25
24
|
process.cwd(),
|
|
26
25
|
'scripts',
|
|
27
26
|
'sync-linear-graphql.js'
|
|
28
27
|
);
|
|
29
28
|
if (fs.existsSync(syncScript)) {
|
|
30
|
-
const { execSync } =
|
|
29
|
+
const { execSync } = require('child_process');
|
|
31
30
|
execSync(`node "${syncScript}"`, {
|
|
32
31
|
stdio: 'ignore',
|
|
33
32
|
timeout: 10000,
|