archicore 0.1.9 → 0.2.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,245 @@
1
+ /**
2
+ * ArchiCore Redis Cache Service
3
+ *
4
+ * Provides caching for API responses to improve performance
5
+ */
6
+ import Redis from 'ioredis';
7
+ import { Logger } from '../../utils/logger.js';
8
+ const DEFAULT_TTL = 300; // 5 minutes
9
+ const CACHE_PREFIX = 'archicore:';
10
+ class CacheService {
11
+ client = null;
12
+ enabled = false;
13
+ memoryCache = new Map();
14
+ async connect() {
15
+ const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
16
+ try {
17
+ this.client = new Redis(redisUrl, {
18
+ maxRetriesPerRequest: 3,
19
+ retryStrategy: (times) => {
20
+ if (times > 3) {
21
+ Logger.warn('Redis connection failed, using memory cache fallback');
22
+ return null;
23
+ }
24
+ return Math.min(times * 100, 3000);
25
+ },
26
+ lazyConnect: true,
27
+ });
28
+ await this.client.connect();
29
+ this.client.on('error', (err) => {
30
+ Logger.error('Redis error:', err.message);
31
+ this.enabled = false;
32
+ });
33
+ this.client.on('connect', () => {
34
+ Logger.info('Redis connected');
35
+ this.enabled = true;
36
+ });
37
+ this.client.on('close', () => {
38
+ Logger.warn('Redis connection closed');
39
+ this.enabled = false;
40
+ });
41
+ // Test connection
42
+ await this.client.ping();
43
+ this.enabled = true;
44
+ Logger.success('Redis cache enabled');
45
+ }
46
+ catch (error) {
47
+ Logger.warn('Redis not available, using memory cache');
48
+ this.enabled = false;
49
+ this.client = null;
50
+ }
51
+ }
52
+ getKey(key, prefix) {
53
+ return `${CACHE_PREFIX}${prefix || ''}${key}`;
54
+ }
55
+ /**
56
+ * Get value from cache
57
+ */
58
+ async get(key, prefix) {
59
+ const fullKey = this.getKey(key, prefix);
60
+ // Try Redis first
61
+ if (this.enabled && this.client) {
62
+ try {
63
+ const data = await this.client.get(fullKey);
64
+ if (data) {
65
+ return JSON.parse(data);
66
+ }
67
+ }
68
+ catch (error) {
69
+ Logger.error('Cache get error:', error);
70
+ }
71
+ }
72
+ // Fallback to memory cache
73
+ const cached = this.memoryCache.get(fullKey);
74
+ if (cached && cached.expires > Date.now()) {
75
+ return cached.data;
76
+ }
77
+ // Clean up expired entry
78
+ if (cached) {
79
+ this.memoryCache.delete(fullKey);
80
+ }
81
+ return null;
82
+ }
83
+ /**
84
+ * Set value in cache
85
+ */
86
+ async set(key, value, options = {}) {
87
+ const fullKey = this.getKey(key, options.prefix);
88
+ const ttl = options.ttl || DEFAULT_TTL;
89
+ const serialized = JSON.stringify(value);
90
+ // Try Redis first
91
+ if (this.enabled && this.client) {
92
+ try {
93
+ await this.client.setex(fullKey, ttl, serialized);
94
+ return;
95
+ }
96
+ catch (error) {
97
+ Logger.error('Cache set error:', error);
98
+ }
99
+ }
100
+ // Fallback to memory cache
101
+ this.memoryCache.set(fullKey, {
102
+ data: value,
103
+ expires: Date.now() + (ttl * 1000),
104
+ });
105
+ // Clean up old memory cache entries periodically
106
+ if (this.memoryCache.size > 1000) {
107
+ this.cleanupMemoryCache();
108
+ }
109
+ }
110
+ /**
111
+ * Delete value from cache
112
+ */
113
+ async del(key, prefix) {
114
+ const fullKey = this.getKey(key, prefix);
115
+ if (this.enabled && this.client) {
116
+ try {
117
+ await this.client.del(fullKey);
118
+ }
119
+ catch (error) {
120
+ Logger.error('Cache del error:', error);
121
+ }
122
+ }
123
+ this.memoryCache.delete(fullKey);
124
+ }
125
+ /**
126
+ * Delete all keys matching pattern
127
+ */
128
+ async delPattern(pattern) {
129
+ const fullPattern = this.getKey(pattern);
130
+ if (this.enabled && this.client) {
131
+ try {
132
+ const keys = await this.client.keys(fullPattern);
133
+ if (keys.length > 0) {
134
+ await this.client.del(...keys);
135
+ }
136
+ }
137
+ catch (error) {
138
+ Logger.error('Cache delPattern error:', error);
139
+ }
140
+ }
141
+ // Clean memory cache matching pattern
142
+ const regex = new RegExp(fullPattern.replace(/\*/g, '.*'));
143
+ for (const key of this.memoryCache.keys()) {
144
+ if (regex.test(key)) {
145
+ this.memoryCache.delete(key);
146
+ }
147
+ }
148
+ }
149
+ /**
150
+ * Clear all cache
151
+ */
152
+ async flush() {
153
+ if (this.enabled && this.client) {
154
+ try {
155
+ const keys = await this.client.keys(`${CACHE_PREFIX}*`);
156
+ if (keys.length > 0) {
157
+ await this.client.del(...keys);
158
+ }
159
+ }
160
+ catch (error) {
161
+ Logger.error('Cache flush error:', error);
162
+ }
163
+ }
164
+ this.memoryCache.clear();
165
+ }
166
+ /**
167
+ * Get or set cache with callback
168
+ */
169
+ async getOrSet(key, callback, options = {}) {
170
+ const cached = await this.get(key, options.prefix);
171
+ if (cached !== null) {
172
+ return cached;
173
+ }
174
+ const value = await callback();
175
+ await this.set(key, value, options);
176
+ return value;
177
+ }
178
+ /**
179
+ * Check if cache is available
180
+ */
181
+ isEnabled() {
182
+ return this.enabled;
183
+ }
184
+ /**
185
+ * Get cache stats
186
+ */
187
+ async getStats() {
188
+ let keys = 0;
189
+ if (this.enabled && this.client) {
190
+ try {
191
+ const allKeys = await this.client.keys(`${CACHE_PREFIX}*`);
192
+ keys = allKeys.length;
193
+ }
194
+ catch (error) {
195
+ Logger.error('Cache stats error:', error);
196
+ }
197
+ return { enabled: true, type: 'redis', keys };
198
+ }
199
+ return {
200
+ enabled: false,
201
+ type: 'memory',
202
+ keys: this.memoryCache.size
203
+ };
204
+ }
205
+ /**
206
+ * Cleanup expired memory cache entries
207
+ */
208
+ cleanupMemoryCache() {
209
+ const now = Date.now();
210
+ for (const [key, value] of this.memoryCache.entries()) {
211
+ if (value.expires < now) {
212
+ this.memoryCache.delete(key);
213
+ }
214
+ }
215
+ }
216
+ /**
217
+ * Disconnect from Redis
218
+ */
219
+ async disconnect() {
220
+ if (this.client) {
221
+ await this.client.quit();
222
+ this.client = null;
223
+ this.enabled = false;
224
+ }
225
+ }
226
+ }
227
+ // Singleton instance
228
+ export const cache = new CacheService();
229
+ // Cache key generators
230
+ export const cacheKeys = {
231
+ project: (projectId) => `project:${projectId}`,
232
+ projectAnalysis: (projectId) => `analysis:${projectId}`,
233
+ projectMetrics: (projectId) => `metrics:${projectId}`,
234
+ projectSearch: (projectId, query) => `search:${projectId}:${query}`,
235
+ user: (userId) => `user:${userId}`,
236
+ userProjects: (userId) => `user-projects:${userId}`,
237
+ };
238
+ // TTL presets (in seconds)
239
+ export const cacheTTL = {
240
+ short: 60, // 1 minute
241
+ medium: 300, // 5 minutes
242
+ long: 3600, // 1 hour
243
+ day: 86400, // 24 hours
244
+ };
245
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * PostgreSQL Database Service for ArchiCore
3
+ *
4
+ * Connection pooling and schema management
5
+ */
6
+ import pg from 'pg';
7
+ declare class DatabaseService {
8
+ private pool;
9
+ private initialized;
10
+ private initPromise;
11
+ /**
12
+ * Initialize database connection pool
13
+ */
14
+ init(): Promise<void>;
15
+ private _init;
16
+ /**
17
+ * Initialize database schema
18
+ */
19
+ private initSchema;
20
+ /**
21
+ * Check if database is available
22
+ */
23
+ isAvailable(): boolean;
24
+ /**
25
+ * Get a client from the pool
26
+ */
27
+ getClient(): Promise<pg.PoolClient>;
28
+ /**
29
+ * Execute a query
30
+ */
31
+ query<T extends pg.QueryResultRow = any>(text: string, params?: any[]): Promise<pg.QueryResult<T>>;
32
+ /**
33
+ * Execute a transaction
34
+ */
35
+ transaction<T>(callback: (client: pg.PoolClient) => Promise<T>): Promise<T>;
36
+ /**
37
+ * Close the connection pool
38
+ */
39
+ close(): Promise<void>;
40
+ }
41
+ export declare const db: DatabaseService;
42
+ export {};
43
+ //# sourceMappingURL=database.d.ts.map
@@ -0,0 +1,221 @@
1
+ /**
2
+ * PostgreSQL Database Service for ArchiCore
3
+ *
4
+ * Connection pooling and schema management
5
+ */
6
+ import pg from 'pg';
7
+ import { Logger } from '../../utils/logger.js';
8
+ const { Pool } = pg;
9
+ // Database schema version for migrations
10
+ const SCHEMA_VERSION = 1;
11
+ // SQL schema for ArchiCore
12
+ const SCHEMA_SQL = `
13
+ -- Users table
14
+ CREATE TABLE IF NOT EXISTS users (
15
+ id VARCHAR(64) PRIMARY KEY,
16
+ email VARCHAR(255) UNIQUE NOT NULL,
17
+ username VARCHAR(100) NOT NULL,
18
+ password_hash VARCHAR(255),
19
+ avatar TEXT,
20
+ tier VARCHAR(20) NOT NULL DEFAULT 'free',
21
+ provider VARCHAR(20) NOT NULL DEFAULT 'email',
22
+ provider_id VARCHAR(255),
23
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
24
+ last_login_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
25
+
26
+ -- Usage tracking (reset daily)
27
+ requests_today INTEGER DEFAULT 0,
28
+ full_analysis_today INTEGER DEFAULT 0,
29
+ projects_today INTEGER DEFAULT 0,
30
+ usage_reset_date DATE DEFAULT CURRENT_DATE,
31
+
32
+ -- Subscription info
33
+ subscription_id VARCHAR(64),
34
+ subscription_status VARCHAR(20),
35
+ subscription_end_date TIMESTAMP WITH TIME ZONE
36
+ );
37
+
38
+ -- Sessions table
39
+ CREATE TABLE IF NOT EXISTS sessions (
40
+ token VARCHAR(128) PRIMARY KEY,
41
+ user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
42
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
43
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
44
+ );
45
+
46
+ -- Audit logs table
47
+ CREATE TABLE IF NOT EXISTS audit_logs (
48
+ id VARCHAR(64) PRIMARY KEY,
49
+ user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL,
50
+ username VARCHAR(100),
51
+ action VARCHAR(50) NOT NULL,
52
+ severity VARCHAR(20) NOT NULL DEFAULT 'info',
53
+ ip VARCHAR(45),
54
+ user_agent TEXT,
55
+ details JSONB,
56
+ success BOOLEAN DEFAULT true,
57
+ error_message TEXT,
58
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
59
+ );
60
+
61
+ -- Projects table (for future use)
62
+ CREATE TABLE IF NOT EXISTS projects (
63
+ id VARCHAR(64) PRIMARY KEY,
64
+ user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
65
+ name VARCHAR(255) NOT NULL,
66
+ description TEXT,
67
+ repo_url TEXT,
68
+ repo_provider VARCHAR(20),
69
+ repo_id VARCHAR(255),
70
+ webhook_secret TEXT,
71
+ last_analyzed_at TIMESTAMP WITH TIME ZONE,
72
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
73
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
74
+ );
75
+
76
+ -- Schema version tracking
77
+ CREATE TABLE IF NOT EXISTS schema_version (
78
+ version INTEGER PRIMARY KEY,
79
+ applied_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
80
+ );
81
+
82
+ -- Indexes for performance
83
+ CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
84
+ CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
85
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
86
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);
87
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at);
88
+ CREATE INDEX IF NOT EXISTS idx_projects_user_id ON projects(user_id);
89
+ CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
90
+ CREATE INDEX IF NOT EXISTS idx_users_provider ON users(provider, provider_id);
91
+ `;
92
+ class DatabaseService {
93
+ pool = null;
94
+ initialized = false;
95
+ initPromise = null;
96
+ /**
97
+ * Initialize database connection pool
98
+ */
99
+ async init() {
100
+ if (this.initialized)
101
+ return;
102
+ if (this.initPromise)
103
+ return this.initPromise;
104
+ this.initPromise = this._init();
105
+ return this.initPromise;
106
+ }
107
+ async _init() {
108
+ const databaseUrl = process.env.DATABASE_URL;
109
+ if (!databaseUrl) {
110
+ Logger.warn('DATABASE_URL not set - using JSON file storage');
111
+ return;
112
+ }
113
+ try {
114
+ this.pool = new Pool({
115
+ connectionString: databaseUrl,
116
+ max: 20, // Maximum pool size
117
+ idleTimeoutMillis: 30000,
118
+ connectionTimeoutMillis: 5000,
119
+ });
120
+ // Test connection
121
+ const client = await this.pool.connect();
122
+ await client.query('SELECT NOW()');
123
+ client.release();
124
+ // Initialize schema
125
+ await this.initSchema();
126
+ this.initialized = true;
127
+ Logger.success('PostgreSQL connected with connection pool (max: 20)');
128
+ }
129
+ catch (error) {
130
+ Logger.error('PostgreSQL connection failed:', error);
131
+ this.pool = null;
132
+ throw error;
133
+ }
134
+ }
135
+ /**
136
+ * Initialize database schema
137
+ */
138
+ async initSchema() {
139
+ if (!this.pool)
140
+ return;
141
+ try {
142
+ // Create schema
143
+ await this.pool.query(SCHEMA_SQL);
144
+ // Check/update schema version
145
+ const result = await this.pool.query('SELECT version FROM schema_version ORDER BY version DESC LIMIT 1');
146
+ if (result.rows.length === 0) {
147
+ await this.pool.query('INSERT INTO schema_version (version) VALUES ($1)', [SCHEMA_VERSION]);
148
+ Logger.info(`Database schema initialized (v${SCHEMA_VERSION})`);
149
+ }
150
+ else {
151
+ const currentVersion = result.rows[0].version;
152
+ if (currentVersion < SCHEMA_VERSION) {
153
+ // Run migrations here if needed
154
+ await this.pool.query('INSERT INTO schema_version (version) VALUES ($1)', [SCHEMA_VERSION]);
155
+ Logger.info(`Database schema migrated to v${SCHEMA_VERSION}`);
156
+ }
157
+ }
158
+ }
159
+ catch (error) {
160
+ Logger.error('Schema initialization failed:', error);
161
+ throw error;
162
+ }
163
+ }
164
+ /**
165
+ * Check if database is available
166
+ */
167
+ isAvailable() {
168
+ return this.pool !== null && this.initialized;
169
+ }
170
+ /**
171
+ * Get a client from the pool
172
+ */
173
+ async getClient() {
174
+ if (!this.pool) {
175
+ throw new Error('Database not initialized');
176
+ }
177
+ return this.pool.connect();
178
+ }
179
+ /**
180
+ * Execute a query
181
+ */
182
+ async query(text, params) {
183
+ if (!this.pool) {
184
+ throw new Error('Database not initialized');
185
+ }
186
+ return this.pool.query(text, params);
187
+ }
188
+ /**
189
+ * Execute a transaction
190
+ */
191
+ async transaction(callback) {
192
+ const client = await this.getClient();
193
+ try {
194
+ await client.query('BEGIN');
195
+ const result = await callback(client);
196
+ await client.query('COMMIT');
197
+ return result;
198
+ }
199
+ catch (error) {
200
+ await client.query('ROLLBACK');
201
+ throw error;
202
+ }
203
+ finally {
204
+ client.release();
205
+ }
206
+ }
207
+ /**
208
+ * Close the connection pool
209
+ */
210
+ async close() {
211
+ if (this.pool) {
212
+ await this.pool.end();
213
+ this.pool = null;
214
+ this.initialized = false;
215
+ Logger.info('Database connection pool closed');
216
+ }
217
+ }
218
+ }
219
+ // Export singleton instance
220
+ export const db = new DatabaseService();
221
+ //# sourceMappingURL=database.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archicore",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "AI Software Architect - code analysis, impact prediction, semantic search",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -25,7 +25,17 @@
25
25
  "cli": "tsx src/cli.ts",
26
26
  "index": "tsx src/cli.ts index",
27
27
  "analyze": "tsx src/cli.ts analyze",
28
- "impact": "tsx src/cli.ts impact"
28
+ "impact": "tsx src/cli.ts impact",
29
+ "docker:build": "docker build -t archicore .",
30
+ "docker:up": "docker compose up -d",
31
+ "docker:down": "docker compose down",
32
+ "docker:logs": "docker compose logs -f archicore",
33
+ "docker:dev": "docker compose -f docker-compose.dev.yml up -d",
34
+ "docker:dev:down": "docker compose -f docker-compose.dev.yml down",
35
+ "typecheck": "tsc --noEmit",
36
+ "docs": "npm --prefix docs-public run start",
37
+ "docs:build": "npm --prefix docs-public run build",
38
+ "docs:install": "npm --prefix docs-public install"
29
39
  },
30
40
  "keywords": [
31
41
  "ai",
@@ -42,6 +52,7 @@
42
52
  "@qdrant/js-client-rest": "^1.11.0",
43
53
  "@types/unzipper": "^0.10.11",
44
54
  "adm-zip": "^0.5.16",
55
+ "bcrypt": "^5.1.1",
45
56
  "boxen": "^8.0.1",
46
57
  "chalk": "^5.4.1",
47
58
  "cli-progress": "^3.12.0",
@@ -55,10 +66,12 @@
55
66
  "figures": "^6.1.0",
56
67
  "glob": "^11.0.0",
57
68
  "helmet": "^8.0.0",
69
+ "ioredis": "^5.9.1",
58
70
  "mime-types": "^3.0.2",
59
71
  "morgan": "^1.10.1",
60
72
  "multer": "^2.0.2",
61
73
  "openai": "^4.73.0",
74
+ "pg": "^8.13.0",
62
75
  "ora": "^8.1.1",
63
76
  "tree-sitter": "^0.21.1",
64
77
  "tree-sitter-javascript": "^0.21.4",
@@ -70,6 +83,7 @@
70
83
  },
71
84
  "devDependencies": {
72
85
  "@types/adm-zip": "^0.5.7",
86
+ "@types/bcrypt": "^5.0.2",
73
87
  "@types/cli-progress": "^3.11.6",
74
88
  "@types/compression": "^1.7.5",
75
89
  "@types/cors": "^2.8.19",
@@ -78,6 +92,7 @@
78
92
  "@types/morgan": "^1.9.10",
79
93
  "@types/multer": "^2.0.0",
80
94
  "@types/node": "^22.10.2",
95
+ "@types/pg": "^8.11.6",
81
96
  "tsx": "^4.19.2",
82
97
  "typescript": "^5.7.2"
83
98
  }