@stackmemoryai/stackmemory 0.5.0 → 0.5.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.
@@ -0,0 +1,328 @@
1
+ import Database from "better-sqlite3";
2
+ import { Pool } from "pg";
3
+ import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
4
+ import { logger } from "../monitoring/logger.js";
5
+ import * as zlib from "zlib";
6
+ import { promisify } from "util";
7
+ const gzipAsync = promisify(zlib.gzip);
8
+ const gunzipAsync = promisify(zlib.gunzip);
9
+ var StorageTier = /* @__PURE__ */ ((StorageTier2) => {
10
+ StorageTier2["HOT"] = "hot";
11
+ StorageTier2["COLD"] = "cold";
12
+ return StorageTier2;
13
+ })(StorageTier || {});
14
+ const DEFAULT_SIMPLIFIED_CONFIG = {
15
+ database: {
16
+ type: process.env["DATABASE_URL"]?.startsWith("postgres") ? "postgresql" : "sqlite",
17
+ url: process.env["DATABASE_URL"] || "./storage/stackmemory.db",
18
+ maxConnections: 10
19
+ },
20
+ objectStorage: {
21
+ endpoint: process.env["S3_ENDPOINT"] || "https://s3.amazonaws.com",
22
+ bucket: process.env["S3_BUCKET"] || "stackmemory-archive",
23
+ accessKeyId: process.env["S3_ACCESS_KEY_ID"] || "",
24
+ secretAccessKey: process.env["S3_SECRET_ACCESS_KEY"] || "",
25
+ region: process.env["S3_REGION"] || "us-east-1"
26
+ },
27
+ tiers: {
28
+ archiveAfterDays: 30,
29
+ // Archive after 30 days instead of complex 3-tier
30
+ compressionThreshold: 1024
31
+ // Compress items > 1KB
32
+ }
33
+ };
34
+ class SimplifiedStorage {
35
+ config;
36
+ db;
37
+ pgPool;
38
+ s3Client;
39
+ isInitialized = false;
40
+ constructor(config = DEFAULT_SIMPLIFIED_CONFIG) {
41
+ this.config = config;
42
+ if (this.config.objectStorage.accessKeyId && this.config.objectStorage.secretAccessKey) {
43
+ this.s3Client = new S3Client({
44
+ endpoint: this.config.objectStorage.endpoint,
45
+ region: this.config.objectStorage.region,
46
+ credentials: {
47
+ accessKeyId: this.config.objectStorage.accessKeyId,
48
+ secretAccessKey: this.config.objectStorage.secretAccessKey
49
+ }
50
+ });
51
+ }
52
+ }
53
+ async initialize() {
54
+ if (this.isInitialized) return;
55
+ try {
56
+ if (this.config.database.type === "postgresql") {
57
+ await this.initializePostgreSQL();
58
+ } else {
59
+ await this.initializeSQLite();
60
+ }
61
+ await this.createTables();
62
+ this.isInitialized = true;
63
+ this.startArchivalProcess();
64
+ logger.info("Simplified storage initialized", {
65
+ databaseType: this.config.database.type,
66
+ objectStorageEnabled: !!this.s3Client
67
+ });
68
+ } catch (error) {
69
+ logger.error("Failed to initialize simplified storage", { error });
70
+ throw error;
71
+ }
72
+ }
73
+ async initializePostgreSQL() {
74
+ this.pgPool = new Pool({
75
+ connectionString: this.config.database.url,
76
+ max: this.config.database.maxConnections,
77
+ idleTimeoutMillis: 3e4
78
+ });
79
+ }
80
+ async initializeSQLite() {
81
+ this.db = new Database(this.config.database.url);
82
+ this.db.pragma("foreign_keys = ON");
83
+ this.db.pragma("journal_mode = WAL");
84
+ }
85
+ async createTables() {
86
+ const schema = `
87
+ CREATE TABLE IF NOT EXISTS storage_items (
88
+ id TEXT PRIMARY KEY,
89
+ data BLOB,
90
+ metadata TEXT,
91
+ tier TEXT NOT NULL,
92
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
93
+ last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
94
+ compressed BOOLEAN DEFAULT FALSE,
95
+ object_key TEXT -- For cold storage reference
96
+ );
97
+ CREATE INDEX IF NOT EXISTS idx_storage_tier ON storage_items(tier);
98
+ CREATE INDEX IF NOT EXISTS idx_storage_created ON storage_items(created_at);
99
+ `;
100
+ if (this.pgPool) {
101
+ await this.pgPool.query(schema.replace(/BLOB/g, "BYTEA").replace(/TIMESTAMP DEFAULT CURRENT_TIMESTAMP/g, "TIMESTAMP DEFAULT NOW()"));
102
+ } else if (this.db) {
103
+ this.db.exec(schema);
104
+ }
105
+ }
106
+ /**
107
+ * Store item in hot tier (database)
108
+ */
109
+ async store(id, data, metadata = {}) {
110
+ if (!this.isInitialized) await this.initialize();
111
+ let processedData = data;
112
+ let compressed = false;
113
+ if (data.length > this.config.tiers.compressionThreshold) {
114
+ processedData = await gzipAsync(data);
115
+ compressed = true;
116
+ logger.debug("Compressed storage item", { id, originalSize: data.length, compressedSize: processedData.length });
117
+ }
118
+ const item = {
119
+ id,
120
+ data: processedData,
121
+ metadata,
122
+ tier: "hot" /* HOT */,
123
+ createdAt: /* @__PURE__ */ new Date(),
124
+ lastAccessed: /* @__PURE__ */ new Date(),
125
+ compressed
126
+ };
127
+ if (this.pgPool) {
128
+ await this.pgPool.query(
129
+ `INSERT INTO storage_items (id, data, metadata, tier, created_at, last_accessed, compressed)
130
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
131
+ ON CONFLICT (id) DO UPDATE SET
132
+ data = EXCLUDED.data,
133
+ metadata = EXCLUDED.metadata,
134
+ last_accessed = EXCLUDED.last_accessed,
135
+ compressed = EXCLUDED.compressed`,
136
+ [id, processedData, JSON.stringify(metadata), item.tier, item.createdAt, item.lastAccessed, compressed]
137
+ );
138
+ } else if (this.db) {
139
+ const stmt = this.db.prepare(`
140
+ INSERT OR REPLACE INTO storage_items (id, data, metadata, tier, created_at, last_accessed, compressed)
141
+ VALUES (?, ?, ?, ?, ?, ?, ?)
142
+ `);
143
+ stmt.run(id, processedData, JSON.stringify(metadata), item.tier, item.createdAt?.toISOString(), item.lastAccessed?.toISOString(), compressed ? 1 : 0);
144
+ }
145
+ logger.debug("Stored item in hot tier", { id, size: processedData.length, compressed });
146
+ }
147
+ /**
148
+ * Retrieve item from appropriate tier
149
+ */
150
+ async retrieve(id) {
151
+ if (!this.isInitialized) await this.initialize();
152
+ await this.updateLastAccessed(id);
153
+ let row;
154
+ if (this.pgPool) {
155
+ const { rows } = await this.pgPool.query("SELECT * FROM storage_items WHERE id = $1", [id]);
156
+ row = rows[0];
157
+ } else if (this.db) {
158
+ row = this.db.prepare("SELECT * FROM storage_items WHERE id = ?").get(id);
159
+ }
160
+ if (!row) return null;
161
+ let data;
162
+ if (row.tier === "cold" /* COLD */ && row.object_key && this.s3Client) {
163
+ data = await this.retrieveFromColdStorage(row.object_key);
164
+ } else {
165
+ data = row.data;
166
+ }
167
+ if (row.compressed) {
168
+ data = await gunzipAsync(data);
169
+ }
170
+ logger.debug("Retrieved item", { id, tier: row.tier, compressed: row.compressed });
171
+ return data;
172
+ }
173
+ /**
174
+ * Archive old items to cold storage
175
+ */
176
+ async archiveOldItems() {
177
+ if (!this.s3Client) return;
178
+ const cutoffDate = /* @__PURE__ */ new Date();
179
+ cutoffDate.setDate(cutoffDate.getDate() - this.config.tiers.archiveAfterDays);
180
+ let rows;
181
+ if (this.pgPool) {
182
+ const { rows: pgRows } = await this.pgPool.query(
183
+ "SELECT * FROM storage_items WHERE tier = $1 AND created_at < $2 LIMIT 100",
184
+ ["hot" /* HOT */, cutoffDate]
185
+ );
186
+ rows = pgRows;
187
+ } else if (this.db) {
188
+ rows = this.db.prepare(
189
+ "SELECT * FROM storage_items WHERE tier = ? AND created_at < ? LIMIT 100"
190
+ ).all("hot" /* HOT */, cutoffDate.toISOString());
191
+ } else {
192
+ return;
193
+ }
194
+ for (const row of rows) {
195
+ try {
196
+ await this.archiveItem(row);
197
+ logger.debug("Archived item to cold storage", { id: row.id });
198
+ } catch (error) {
199
+ logger.warn("Failed to archive item", { id: row.id, error });
200
+ }
201
+ }
202
+ if (rows.length > 0) {
203
+ logger.info("Archived old items", { count: rows.length, cutoffDate });
204
+ }
205
+ }
206
+ async archiveItem(row) {
207
+ if (!this.s3Client) return;
208
+ const objectKey = `archived/${row.id}`;
209
+ await this.s3Client.send(
210
+ new PutObjectCommand({
211
+ Bucket: this.config.objectStorage.bucket,
212
+ Key: objectKey,
213
+ Body: row.data,
214
+ Metadata: {
215
+ originalId: row.id,
216
+ compressed: row.compressed.toString(),
217
+ archivedAt: (/* @__PURE__ */ new Date()).toISOString()
218
+ }
219
+ })
220
+ );
221
+ if (this.pgPool) {
222
+ await this.pgPool.query(
223
+ "UPDATE storage_items SET tier = $1, data = NULL, object_key = $2 WHERE id = $3",
224
+ ["cold" /* COLD */, objectKey, row.id]
225
+ );
226
+ } else if (this.db) {
227
+ this.db.prepare(
228
+ "UPDATE storage_items SET tier = ?, data = NULL, object_key = ? WHERE id = ?"
229
+ ).run("cold" /* COLD */, objectKey, row.id);
230
+ }
231
+ }
232
+ async retrieveFromColdStorage(objectKey) {
233
+ if (!this.s3Client) throw new Error("Object storage not configured");
234
+ const response = await this.s3Client.send(
235
+ new GetObjectCommand({
236
+ Bucket: this.config.objectStorage.bucket,
237
+ Key: objectKey
238
+ })
239
+ );
240
+ if (!response.Body) throw new Error("Empty response from cold storage");
241
+ const chunks = [];
242
+ const reader = response.Body;
243
+ return new Promise((resolve, reject) => {
244
+ reader.on("data", (chunk) => chunks.push(chunk));
245
+ reader.on("end", () => resolve(Buffer.concat(chunks)));
246
+ reader.on("error", reject);
247
+ });
248
+ }
249
+ async updateLastAccessed(id) {
250
+ const now = /* @__PURE__ */ new Date();
251
+ if (this.pgPool) {
252
+ await this.pgPool.query(
253
+ "UPDATE storage_items SET last_accessed = $1 WHERE id = $2",
254
+ [now, id]
255
+ );
256
+ } else if (this.db) {
257
+ this.db.prepare("UPDATE storage_items SET last_accessed = ? WHERE id = ?").run(now.toISOString(), id);
258
+ }
259
+ }
260
+ startArchivalProcess() {
261
+ setInterval(async () => {
262
+ try {
263
+ await this.archiveOldItems();
264
+ } catch (error) {
265
+ logger.error("Archival process failed", { error });
266
+ }
267
+ }, 6 * 60 * 60 * 1e3);
268
+ logger.info("Started archival process", { intervalHours: 6 });
269
+ }
270
+ /**
271
+ * Get storage statistics
272
+ */
273
+ async getStats() {
274
+ if (!this.isInitialized) await this.initialize();
275
+ let hotItems = 0;
276
+ let coldItems = 0;
277
+ let totalSize = 0;
278
+ if (this.pgPool) {
279
+ const { rows } = await this.pgPool.query(`
280
+ SELECT tier, COUNT(*) as count, SUM(length(data)) as size
281
+ FROM storage_items
282
+ GROUP BY tier
283
+ `);
284
+ for (const row of rows) {
285
+ if (row.tier === "hot" /* HOT */) {
286
+ hotItems = parseInt(row.count);
287
+ totalSize += parseInt(row.size || 0);
288
+ } else {
289
+ coldItems = parseInt(row.count);
290
+ }
291
+ }
292
+ } else if (this.db) {
293
+ const stmt = this.db.prepare(`
294
+ SELECT tier, COUNT(*) as count, SUM(length(data)) as size
295
+ FROM storage_items
296
+ GROUP BY tier
297
+ `);
298
+ const rows = stmt.all();
299
+ for (const row of rows) {
300
+ if (row.tier === "hot" /* HOT */) {
301
+ hotItems = row.count;
302
+ totalSize += row.size || 0;
303
+ } else {
304
+ coldItems = row.count;
305
+ }
306
+ }
307
+ }
308
+ return { hotItems, coldItems, totalSize };
309
+ }
310
+ async close() {
311
+ if (this.pgPool) {
312
+ await this.pgPool.end();
313
+ }
314
+ if (this.db) {
315
+ this.db.close();
316
+ }
317
+ this.isInitialized = false;
318
+ logger.info("Simplified storage closed");
319
+ }
320
+ }
321
+ var simplified_storage_default = SimplifiedStorage;
322
+ export {
323
+ DEFAULT_SIMPLIFIED_CONFIG,
324
+ SimplifiedStorage,
325
+ StorageTier,
326
+ simplified_storage_default as default
327
+ };
328
+ //# sourceMappingURL=simplified-storage.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/core/storage/simplified-storage.ts"],
4
+ "sourcesContent": ["/**\n * Simplified 2-Tier Storage System\n * Tier 1: SQLite/PostgreSQL (Hot) - Active data, immediate access\n * Tier 2: Object Storage (Cold) - Archive data, cost-effective long-term storage\n * \n * ARCHITECT RECOMMENDATION: Removed Redis complexity, GCS overkill\n */\n\nimport Database from 'better-sqlite3';\nimport { Pool } from 'pg';\nimport { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';\nimport { logger } from '../monitoring/logger.js';\nimport * as zlib from 'zlib';\nimport { promisify } from 'util';\n\nconst gzipAsync = promisify(zlib.gzip);\nconst gunzipAsync = promisify(zlib.gunzip);\n\nexport enum StorageTier {\n HOT = 'hot', // SQLite/PostgreSQL: Active data\n COLD = 'cold', // Object Storage: Archive data\n}\n\nexport interface SimplifiedStorageConfig {\n database: {\n type: 'sqlite' | 'postgresql';\n url: string;\n maxConnections?: number;\n };\n objectStorage: {\n endpoint: string;\n bucket: string;\n accessKeyId: string;\n secretAccessKey: string;\n region: string;\n };\n tiers: {\n archiveAfterDays: number; // Move to cold storage after N days\n compressionThreshold: number; // Compress items larger than N bytes\n };\n}\n\nexport const DEFAULT_SIMPLIFIED_CONFIG: SimplifiedStorageConfig = {\n database: {\n type: process.env['DATABASE_URL']?.startsWith('postgres') ? 'postgresql' : 'sqlite',\n url: process.env['DATABASE_URL'] || './storage/stackmemory.db',\n maxConnections: 10,\n },\n objectStorage: {\n endpoint: process.env['S3_ENDPOINT'] || 'https://s3.amazonaws.com',\n bucket: process.env['S3_BUCKET'] || 'stackmemory-archive',\n accessKeyId: process.env['S3_ACCESS_KEY_ID'] || '',\n secretAccessKey: process.env['S3_SECRET_ACCESS_KEY'] || '',\n region: process.env['S3_REGION'] || 'us-east-1',\n },\n tiers: {\n archiveAfterDays: 30, // Archive after 30 days instead of complex 3-tier\n compressionThreshold: 1024, // Compress items > 1KB\n },\n};\n\nexport interface StoredItem {\n id: string;\n data: Buffer;\n metadata: Record<string, unknown>;\n tier: StorageTier;\n createdAt: Date;\n lastAccessed: Date;\n compressed: boolean;\n}\n\ninterface DatabaseRow {\n id: string;\n data: Buffer;\n metadata: string;\n tier: string;\n created_at: string;\n last_accessed: string;\n compressed: number | boolean;\n object_key?: string;\n}\n\n/**\n * Simplified Storage System (Architect-approved)\n * Removes Redis complexity, GCS overkill, premature optimization\n */\nexport class SimplifiedStorage {\n private config: SimplifiedStorageConfig;\n private db?: Database.Database;\n private pgPool?: Pool;\n private s3Client?: S3Client;\n private isInitialized = false;\n\n constructor(config: SimplifiedStorageConfig = DEFAULT_SIMPLIFIED_CONFIG) {\n this.config = config;\n \n // Only initialize S3 if properly configured\n if (this.config.objectStorage.accessKeyId && this.config.objectStorage.secretAccessKey) {\n this.s3Client = new S3Client({\n endpoint: this.config.objectStorage.endpoint,\n region: this.config.objectStorage.region,\n credentials: {\n accessKeyId: this.config.objectStorage.accessKeyId,\n secretAccessKey: this.config.objectStorage.secretAccessKey,\n },\n });\n }\n }\n\n async initialize(): Promise<void> {\n if (this.isInitialized) return;\n\n try {\n if (this.config.database.type === 'postgresql') {\n await this.initializePostgreSQL();\n } else {\n await this.initializeSQLite();\n }\n\n await this.createTables();\n this.isInitialized = true;\n \n // Start background archival process (simple cron-like)\n this.startArchivalProcess();\n \n logger.info('Simplified storage initialized', {\n databaseType: this.config.database.type,\n objectStorageEnabled: !!this.s3Client,\n });\n } catch (error) {\n logger.error('Failed to initialize simplified storage', { error });\n throw error;\n }\n }\n\n private async initializePostgreSQL(): Promise<void> {\n this.pgPool = new Pool({\n connectionString: this.config.database.url,\n max: this.config.database.maxConnections,\n idleTimeoutMillis: 30000,\n });\n }\n\n private async initializeSQLite(): Promise<void> {\n this.db = new Database(this.config.database.url);\n this.db.pragma('foreign_keys = ON');\n this.db.pragma('journal_mode = WAL');\n }\n\n private async createTables(): Promise<void> {\n const schema = `\n CREATE TABLE IF NOT EXISTS storage_items (\n id TEXT PRIMARY KEY,\n data BLOB,\n metadata TEXT,\n tier TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_accessed TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n compressed BOOLEAN DEFAULT FALSE,\n object_key TEXT -- For cold storage reference\n );\n CREATE INDEX IF NOT EXISTS idx_storage_tier ON storage_items(tier);\n CREATE INDEX IF NOT EXISTS idx_storage_created ON storage_items(created_at);\n `;\n\n if (this.pgPool) {\n await this.pgPool.query(schema.replace(/BLOB/g, 'BYTEA').replace(/TIMESTAMP DEFAULT CURRENT_TIMESTAMP/g, 'TIMESTAMP DEFAULT NOW()'));\n } else if (this.db) {\n this.db.exec(schema);\n }\n }\n\n /**\n * Store item in hot tier (database)\n */\n async store(id: string, data: Buffer, metadata: Record<string, unknown> = {}): Promise<void> {\n if (!this.isInitialized) await this.initialize();\n\n let processedData = data;\n let compressed = false;\n\n // Compress if over threshold\n if (data.length > this.config.tiers.compressionThreshold) {\n processedData = await gzipAsync(data);\n compressed = true;\n logger.debug('Compressed storage item', { id, originalSize: data.length, compressedSize: processedData.length });\n }\n\n const item: Partial<StoredItem> = {\n id,\n data: processedData,\n metadata,\n tier: StorageTier.HOT,\n createdAt: new Date(),\n lastAccessed: new Date(),\n compressed,\n };\n\n if (this.pgPool) {\n await this.pgPool.query(\n `INSERT INTO storage_items (id, data, metadata, tier, created_at, last_accessed, compressed)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT (id) DO UPDATE SET\n data = EXCLUDED.data,\n metadata = EXCLUDED.metadata,\n last_accessed = EXCLUDED.last_accessed,\n compressed = EXCLUDED.compressed`,\n [id, processedData, JSON.stringify(metadata), item.tier, item.createdAt, item.lastAccessed, compressed]\n );\n } else if (this.db) {\n const stmt = this.db.prepare(`\n INSERT OR REPLACE INTO storage_items (id, data, metadata, tier, created_at, last_accessed, compressed)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n stmt.run(id, processedData, JSON.stringify(metadata), item.tier, item.createdAt?.toISOString(), item.lastAccessed?.toISOString(), compressed ? 1 : 0);\n }\n\n logger.debug('Stored item in hot tier', { id, size: processedData.length, compressed });\n }\n\n /**\n * Retrieve item from appropriate tier\n */\n async retrieve(id: string): Promise<Buffer | null> {\n if (!this.isInitialized) await this.initialize();\n\n // Update last accessed timestamp\n await this.updateLastAccessed(id);\n\n let row: DatabaseRow | undefined;\n if (this.pgPool) {\n const { rows } = await this.pgPool.query('SELECT * FROM storage_items WHERE id = $1', [id]);\n row = rows[0];\n } else if (this.db) {\n row = this.db.prepare('SELECT * FROM storage_items WHERE id = ?').get(id);\n }\n\n if (!row) return null;\n\n let data: Buffer;\n \n if (row.tier === StorageTier.COLD && row.object_key && this.s3Client) {\n // Retrieve from cold storage\n data = await this.retrieveFromColdStorage(row.object_key);\n } else {\n // Retrieve from hot storage\n data = row.data;\n }\n\n // Decompress if needed\n if (row.compressed) {\n data = await gunzipAsync(data);\n }\n\n logger.debug('Retrieved item', { id, tier: row.tier, compressed: row.compressed });\n return data;\n }\n\n /**\n * Archive old items to cold storage\n */\n private async archiveOldItems(): Promise<void> {\n if (!this.s3Client) return; // Skip if object storage not configured\n\n const cutoffDate = new Date();\n cutoffDate.setDate(cutoffDate.getDate() - this.config.tiers.archiveAfterDays);\n\n let rows: DatabaseRow[];\n if (this.pgPool) {\n const { rows: pgRows } = await this.pgPool.query(\n 'SELECT * FROM storage_items WHERE tier = $1 AND created_at < $2 LIMIT 100',\n [StorageTier.HOT, cutoffDate]\n );\n rows = pgRows;\n } else if (this.db) {\n rows = this.db.prepare(\n 'SELECT * FROM storage_items WHERE tier = ? AND created_at < ? LIMIT 100'\n ).all(StorageTier.HOT, cutoffDate.toISOString());\n } else {\n return;\n }\n\n for (const row of rows) {\n try {\n await this.archiveItem(row);\n logger.debug('Archived item to cold storage', { id: row.id });\n } catch (error) {\n logger.warn('Failed to archive item', { id: row.id, error });\n }\n }\n\n if (rows.length > 0) {\n logger.info('Archived old items', { count: rows.length, cutoffDate });\n }\n }\n\n private async archiveItem(row: DatabaseRow): Promise<void> {\n if (!this.s3Client) return;\n\n const objectKey = `archived/${row.id}`;\n \n // Upload to S3\n await this.s3Client.send(\n new PutObjectCommand({\n Bucket: this.config.objectStorage.bucket,\n Key: objectKey,\n Body: row.data,\n Metadata: {\n originalId: row.id,\n compressed: row.compressed.toString(),\n archivedAt: new Date().toISOString(),\n },\n })\n );\n\n // Update database record\n if (this.pgPool) {\n await this.pgPool.query(\n 'UPDATE storage_items SET tier = $1, data = NULL, object_key = $2 WHERE id = $3',\n [StorageTier.COLD, objectKey, row.id]\n );\n } else if (this.db) {\n this.db.prepare(\n 'UPDATE storage_items SET tier = ?, data = NULL, object_key = ? WHERE id = ?'\n ).run(StorageTier.COLD, objectKey, row.id);\n }\n }\n\n private async retrieveFromColdStorage(objectKey: string): Promise<Buffer> {\n if (!this.s3Client) throw new Error('Object storage not configured');\n\n const response = await this.s3Client.send(\n new GetObjectCommand({\n Bucket: this.config.objectStorage.bucket,\n Key: objectKey,\n })\n );\n\n if (!response.Body) throw new Error('Empty response from cold storage');\n\n // Convert stream to buffer\n const chunks: Buffer[] = [];\n const reader = response.Body as NodeJS.ReadableStream;\n \n return new Promise((resolve, reject) => {\n reader.on('data', (chunk: Buffer) => chunks.push(chunk));\n reader.on('end', () => resolve(Buffer.concat(chunks)));\n reader.on('error', reject);\n });\n }\n\n private async updateLastAccessed(id: string): Promise<void> {\n const now = new Date();\n \n if (this.pgPool) {\n await this.pgPool.query(\n 'UPDATE storage_items SET last_accessed = $1 WHERE id = $2',\n [now, id]\n );\n } else if (this.db) {\n this.db.prepare('UPDATE storage_items SET last_accessed = ? WHERE id = ?')\n .run(now.toISOString(), id);\n }\n }\n\n private startArchivalProcess(): void {\n // Run archival every 6 hours instead of complex real-time monitoring\n setInterval(async () => {\n try {\n await this.archiveOldItems();\n } catch (error) {\n logger.error('Archival process failed', { error });\n }\n }, 6 * 60 * 60 * 1000); // 6 hours\n\n logger.info('Started archival process', { intervalHours: 6 });\n }\n\n /**\n * Get storage statistics\n */\n async getStats(): Promise<{ hotItems: number; coldItems: number; totalSize: number }> {\n if (!this.isInitialized) await this.initialize();\n\n let hotItems = 0;\n let coldItems = 0;\n let totalSize = 0;\n\n if (this.pgPool) {\n const { rows } = await this.pgPool.query(`\n SELECT tier, COUNT(*) as count, SUM(length(data)) as size\n FROM storage_items\n GROUP BY tier\n `);\n \n for (const row of rows) {\n if (row.tier === StorageTier.HOT) {\n hotItems = parseInt(row.count);\n totalSize += parseInt(row.size || 0);\n } else {\n coldItems = parseInt(row.count);\n }\n }\n } else if (this.db) {\n const stmt = this.db.prepare(`\n SELECT tier, COUNT(*) as count, SUM(length(data)) as size\n FROM storage_items\n GROUP BY tier\n `);\n const rows = stmt.all();\n \n for (const row of rows) {\n if (row.tier === StorageTier.HOT) {\n hotItems = row.count;\n totalSize += row.size || 0;\n } else {\n coldItems = row.count;\n }\n }\n }\n\n return { hotItems, coldItems, totalSize };\n }\n\n async close(): Promise<void> {\n if (this.pgPool) {\n await this.pgPool.end();\n }\n if (this.db) {\n this.db.close();\n }\n this.isInitialized = false;\n logger.info('Simplified storage closed');\n }\n}\n\nexport default SimplifiedStorage;"],
5
+ "mappings": "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",
6
+ "names": ["StorageTier"]
7
+ }
@@ -0,0 +1,166 @@
1
+ import { LinearClient } from "@linear/sdk";
2
+ import { logger } from "../../core/monitoring/logger.js";
3
+ class LinearPlugin {
4
+ name = "linear-integration";
5
+ version = "1.0.0";
6
+ description = "Linear task synchronization for StackMemory";
7
+ client = null;
8
+ apiKey = null;
9
+ async initialize() {
10
+ this.apiKey = process.env["LINEAR_API_KEY"] || null;
11
+ if (!this.apiKey) {
12
+ logger.info("Linear plugin: No API key found, running in offline mode");
13
+ return;
14
+ }
15
+ try {
16
+ this.client = new LinearClient({ apiKey: this.apiKey });
17
+ await this.client.me;
18
+ logger.info("Linear plugin initialized successfully");
19
+ } catch (error) {
20
+ logger.warn("Linear plugin: Failed to connect", error);
21
+ this.client = null;
22
+ }
23
+ }
24
+ async shutdown() {
25
+ this.client = null;
26
+ logger.info("Linear plugin shut down");
27
+ }
28
+ registerCommands() {
29
+ return [
30
+ {
31
+ name: "linear-sync",
32
+ description: "Sync tasks with Linear",
33
+ action: this.syncTasks.bind(this),
34
+ options: [
35
+ {
36
+ flag: "--team <id>",
37
+ description: "Linear team ID"
38
+ },
39
+ {
40
+ flag: "--project <id>",
41
+ description: "Linear project ID"
42
+ }
43
+ ]
44
+ },
45
+ {
46
+ name: "linear-create",
47
+ description: "Create a Linear issue from current context",
48
+ action: this.createIssue.bind(this),
49
+ options: [
50
+ {
51
+ flag: "--title <title>",
52
+ description: "Issue title"
53
+ },
54
+ {
55
+ flag: "--description <desc>",
56
+ description: "Issue description"
57
+ }
58
+ ]
59
+ }
60
+ ];
61
+ }
62
+ registerTools() {
63
+ return [
64
+ {
65
+ name: "linear_sync",
66
+ description: "Sync current context with Linear tasks",
67
+ inputSchema: {
68
+ type: "object",
69
+ properties: {
70
+ teamId: { type: "string", description: "Linear team ID" },
71
+ projectId: { type: "string", description: "Linear project ID" }
72
+ }
73
+ },
74
+ handler: async (input) => {
75
+ return await this.syncTasks(input);
76
+ }
77
+ },
78
+ {
79
+ name: "linear_create_issue",
80
+ description: "Create a Linear issue",
81
+ inputSchema: {
82
+ type: "object",
83
+ properties: {
84
+ title: { type: "string" },
85
+ description: { type: "string" },
86
+ teamId: { type: "string" }
87
+ },
88
+ required: ["title"]
89
+ },
90
+ handler: async (input) => {
91
+ return await this.createIssue(input);
92
+ }
93
+ }
94
+ ];
95
+ }
96
+ onFrameCreated(frame) {
97
+ if (frame.type === "task" && this.client) {
98
+ this.syncFrameToLinear(frame).catch((error) => {
99
+ logger.debug("Failed to sync frame to Linear", error);
100
+ });
101
+ }
102
+ }
103
+ async syncTasks(options) {
104
+ if (!this.client) {
105
+ return { success: false, message: "Linear not connected" };
106
+ }
107
+ try {
108
+ const issues = await this.client.issues({
109
+ filter: {
110
+ team: { id: { eq: options.teamId } }
111
+ }
112
+ });
113
+ return {
114
+ success: true,
115
+ synced: issues.nodes.length,
116
+ message: `Synced ${issues.nodes.length} issues from Linear`
117
+ };
118
+ } catch (error) {
119
+ logger.error("Linear sync failed", error);
120
+ return { success: false, error: error.message };
121
+ }
122
+ }
123
+ async createIssue(options) {
124
+ if (!this.client) {
125
+ return { success: false, message: "Linear not connected" };
126
+ }
127
+ try {
128
+ const issue = await this.client.createIssue({
129
+ title: options.title,
130
+ description: options.description,
131
+ teamId: options.teamId || await this.getDefaultTeamId()
132
+ });
133
+ return {
134
+ success: true,
135
+ issueId: issue.issue?.id,
136
+ url: issue.issue?.url,
137
+ message: `Created Linear issue: ${issue.issue?.identifier}`
138
+ };
139
+ } catch (error) {
140
+ logger.error("Failed to create Linear issue", error);
141
+ return { success: false, error: error.message };
142
+ }
143
+ }
144
+ async syncFrameToLinear(frame) {
145
+ if (frame.name && frame.content) {
146
+ await this.createIssue({
147
+ title: frame.name,
148
+ description: frame.content
149
+ });
150
+ }
151
+ }
152
+ async getDefaultTeamId() {
153
+ if (!this.client) throw new Error("Linear not connected");
154
+ const teams = await this.client.teams();
155
+ if (teams.nodes.length > 0) {
156
+ return teams.nodes[0].id;
157
+ }
158
+ throw new Error("No Linear teams found");
159
+ }
160
+ }
161
+ var linear_default = LinearPlugin;
162
+ export {
163
+ LinearPlugin,
164
+ linear_default as default
165
+ };
166
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/plugins/linear/index.ts"],
4
+ "sourcesContent": ["/**\n * Linear Integration Plugin for StackMemory\n * Provides task synchronization with Linear as an optional feature\n */\n\nimport { StackMemoryPlugin, PluginCommand, PluginTool } from '../plugin-interface.js';\nimport { LinearClient } from '@linear/sdk';\nimport { logger } from '../../core/monitoring/logger.js';\n\nexport class LinearPlugin implements StackMemoryPlugin {\n name = 'linear-integration';\n version = '1.0.0';\n description = 'Linear task synchronization for StackMemory';\n \n private client: LinearClient | null = null;\n private apiKey: string | null = null;\n \n async initialize(): Promise<void> {\n this.apiKey = process.env['LINEAR_API_KEY'] || null;\n \n if (!this.apiKey) {\n logger.info('Linear plugin: No API key found, running in offline mode');\n return;\n }\n \n try {\n this.client = new LinearClient({ apiKey: this.apiKey });\n await this.client.me; // Test connection\n logger.info('Linear plugin initialized successfully');\n } catch (error) {\n logger.warn('Linear plugin: Failed to connect', error);\n this.client = null;\n }\n }\n \n async shutdown(): Promise<void> {\n this.client = null;\n logger.info('Linear plugin shut down');\n }\n \n registerCommands(): PluginCommand[] {\n return [\n {\n name: 'linear-sync',\n description: 'Sync tasks with Linear',\n action: this.syncTasks.bind(this),\n options: [\n {\n flag: '--team <id>',\n description: 'Linear team ID',\n },\n {\n flag: '--project <id>',\n description: 'Linear project ID',\n }\n ]\n },\n {\n name: 'linear-create',\n description: 'Create a Linear issue from current context',\n action: this.createIssue.bind(this),\n options: [\n {\n flag: '--title <title>',\n description: 'Issue title',\n },\n {\n flag: '--description <desc>',\n description: 'Issue description',\n }\n ]\n }\n ];\n }\n \n registerTools(): PluginTool[] {\n return [\n {\n name: 'linear_sync',\n description: 'Sync current context with Linear tasks',\n inputSchema: {\n type: 'object',\n properties: {\n teamId: { type: 'string', description: 'Linear team ID' },\n projectId: { type: 'string', description: 'Linear project ID' }\n }\n },\n handler: async (input) => {\n return await this.syncTasks(input);\n }\n },\n {\n name: 'linear_create_issue',\n description: 'Create a Linear issue',\n inputSchema: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n description: { type: 'string' },\n teamId: { type: 'string' }\n },\n required: ['title']\n },\n handler: async (input) => {\n return await this.createIssue(input);\n }\n }\n ];\n }\n \n onFrameCreated(frame: any): void {\n // Auto-sync with Linear when a task frame is created\n if (frame.type === 'task' && this.client) {\n this.syncFrameToLinear(frame).catch(error => {\n logger.debug('Failed to sync frame to Linear', error);\n });\n }\n }\n \n private async syncTasks(options: any): Promise<any> {\n if (!this.client) {\n return { success: false, message: 'Linear not connected' };\n }\n \n try {\n // Basic sync implementation\n const issues = await this.client.issues({\n filter: {\n team: { id: { eq: options.teamId } }\n }\n });\n \n return {\n success: true,\n synced: issues.nodes.length,\n message: `Synced ${issues.nodes.length} issues from Linear`\n };\n } catch (error) {\n logger.error('Linear sync failed', error);\n return { success: false, error: (error as Error).message };\n }\n }\n \n private async createIssue(options: any): Promise<any> {\n if (!this.client) {\n return { success: false, message: 'Linear not connected' };\n }\n \n try {\n const issue = await this.client.createIssue({\n title: options.title,\n description: options.description,\n teamId: options.teamId || (await this.getDefaultTeamId())\n });\n \n return {\n success: true,\n issueId: issue.issue?.id,\n url: issue.issue?.url,\n message: `Created Linear issue: ${issue.issue?.identifier}`\n };\n } catch (error) {\n logger.error('Failed to create Linear issue', error);\n return { success: false, error: (error as Error).message };\n }\n }\n \n private async syncFrameToLinear(frame: any): Promise<void> {\n // Simple frame to Linear sync\n if (frame.name && frame.content) {\n await this.createIssue({\n title: frame.name,\n description: frame.content\n });\n }\n }\n \n private async getDefaultTeamId(): Promise<string> {\n if (!this.client) throw new Error('Linear not connected');\n \n const teams = await this.client.teams();\n if (teams.nodes.length > 0) {\n return teams.nodes[0].id;\n }\n throw new Error('No Linear teams found');\n }\n}\n\n// Export for plugin registration\nexport default LinearPlugin;"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,57 @@
1
+ import { pluginManager } from "./plugin-interface.js";
2
+ import { logger } from "../core/monitoring/logger.js";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { join } from "path";
5
+ async function loadPlugins() {
6
+ const configPath = join(process.cwd(), ".stackmemory", "plugins.json");
7
+ let enabledPlugins = [];
8
+ if (existsSync(configPath)) {
9
+ try {
10
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
11
+ enabledPlugins = config.enabled || [];
12
+ } catch (error) {
13
+ logger.warn("Failed to load plugin configuration", error);
14
+ }
15
+ } else {
16
+ enabledPlugins = [];
17
+ if (process.env["ENABLE_LINEAR_PLUGIN"] === "true") {
18
+ enabledPlugins.push("linear");
19
+ }
20
+ }
21
+ for (const pluginName of enabledPlugins) {
22
+ try {
23
+ await loadPlugin(pluginName);
24
+ } catch (error) {
25
+ logger.warn(`Failed to load plugin ${pluginName}`, error);
26
+ }
27
+ }
28
+ const loadedPlugins = pluginManager.getAllPlugins();
29
+ if (loadedPlugins.length > 0) {
30
+ logger.info(
31
+ `Loaded ${loadedPlugins.length} plugins: ${loadedPlugins.map((p) => p.name).join(", ")}`
32
+ );
33
+ }
34
+ }
35
+ async function loadPlugin(name) {
36
+ switch (name) {
37
+ case "linear": {
38
+ const { LinearPlugin } = await import("./linear/index.js");
39
+ const plugin = new LinearPlugin();
40
+ await pluginManager.register(plugin);
41
+ break;
42
+ }
43
+ // Future plugins can be added here
44
+ // case 'github': {
45
+ // const { GitHubPlugin } = await import('./github/index.js');
46
+ // await pluginManager.register(new GitHubPlugin());
47
+ // break;
48
+ // }
49
+ default:
50
+ logger.warn(`Unknown plugin: ${name}`);
51
+ }
52
+ }
53
+ export {
54
+ loadPlugins,
55
+ pluginManager
56
+ };
57
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/plugins/loader.ts"],
4
+ "sourcesContent": ["/**\n * Plugin Loader for StackMemory\n * Automatically loads plugins based on configuration\n */\n\nimport { pluginManager } from './plugin-interface.js';\nimport { logger } from '../core/monitoring/logger.js';\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport async function loadPlugins(): Promise<void> {\n // Check for plugin configuration\n const configPath = join(process.cwd(), '.stackmemory', 'plugins.json');\n\n let enabledPlugins: string[] = [];\n\n if (existsSync(configPath)) {\n try {\n const config = JSON.parse(readFileSync(configPath, 'utf-8'));\n enabledPlugins = config.enabled || [];\n } catch (error) {\n logger.warn('Failed to load plugin configuration', error);\n }\n } else {\n // Default plugins (none by default, all optional)\n enabledPlugins = [];\n\n // Check environment variables for opt-in plugins\n if (process.env['ENABLE_LINEAR_PLUGIN'] === 'true') {\n enabledPlugins.push('linear');\n }\n }\n\n // Load enabled plugins\n for (const pluginName of enabledPlugins) {\n try {\n await loadPlugin(pluginName);\n } catch (error) {\n logger.warn(`Failed to load plugin ${pluginName}`, error);\n }\n }\n\n const loadedPlugins = pluginManager.getAllPlugins();\n if (loadedPlugins.length > 0) {\n logger.info(\n `Loaded ${loadedPlugins.length} plugins: ${loadedPlugins.map((p) => p.name).join(', ')}`\n );\n }\n}\n\nasync function loadPlugin(name: string): Promise<void> {\n switch (name) {\n case 'linear': {\n const { LinearPlugin } = await import('./linear/index.js');\n const plugin = new LinearPlugin();\n await pluginManager.register(plugin);\n break;\n }\n\n // Future plugins can be added here\n // case 'github': {\n // const { GitHubPlugin } = await import('./github/index.js');\n // await pluginManager.register(new GitHubPlugin());\n // break;\n // }\n\n default:\n logger.warn(`Unknown plugin: ${name}`);\n }\n}\n\nexport { pluginManager };\n"],
5
+ "mappings": "AAKA,SAAS,qBAAqB;AAC9B,SAAS,cAAc;AACvB,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AAErB,eAAsB,cAA6B;AAEjD,QAAM,aAAa,KAAK,QAAQ,IAAI,GAAG,gBAAgB,cAAc;AAErE,MAAI,iBAA2B,CAAC;AAEhC,MAAI,WAAW,UAAU,GAAG;AAC1B,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,aAAa,YAAY,OAAO,CAAC;AAC3D,uBAAiB,OAAO,WAAW,CAAC;AAAA,IACtC,SAAS,OAAO;AACd,aAAO,KAAK,uCAAuC,KAAK;AAAA,IAC1D;AAAA,EACF,OAAO;AAEL,qBAAiB,CAAC;AAGlB,QAAI,QAAQ,IAAI,sBAAsB,MAAM,QAAQ;AAClD,qBAAe,KAAK,QAAQ;AAAA,IAC9B;AAAA,EACF;AAGA,aAAW,cAAc,gBAAgB;AACvC,QAAI;AACF,YAAM,WAAW,UAAU;AAAA,IAC7B,SAAS,OAAO;AACd,aAAO,KAAK,yBAAyB,UAAU,IAAI,KAAK;AAAA,IAC1D;AAAA,EACF;AAEA,QAAM,gBAAgB,cAAc,cAAc;AAClD,MAAI,cAAc,SAAS,GAAG;AAC5B,WAAO;AAAA,MACL,UAAU,cAAc,MAAM,aAAa,cAAc,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;AAAA,IACxF;AAAA,EACF;AACF;AAEA,eAAe,WAAW,MAA6B;AACrD,UAAQ,MAAM;AAAA,IACZ,KAAK,UAAU;AACb,YAAM,EAAE,aAAa,IAAI,MAAM,OAAO,mBAAmB;AACzD,YAAM,SAAS,IAAI,aAAa;AAChC,YAAM,cAAc,SAAS,MAAM;AACnC;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA;AACE,aAAO,KAAK,mBAAmB,IAAI,EAAE;AAAA,EACzC;AACF;",
6
+ "names": []
7
+ }