@siftd/connect-agent 0.2.10 → 0.2.11

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,66 @@
1
+ /**
2
+ * Memory Store Interface
3
+ *
4
+ * Common interface for memory backends (SQLite, Postgres, etc.)
5
+ */
6
+ export type MemoryType = 'episodic' | 'semantic' | 'procedural' | 'working';
7
+ export interface Memory {
8
+ id: string;
9
+ type: MemoryType;
10
+ content: string;
11
+ summary?: string;
12
+ source: string;
13
+ timestamp: string;
14
+ lastAccessed: string;
15
+ importance: number;
16
+ accessCount: number;
17
+ decayRate: number;
18
+ associations: string[];
19
+ tags: string[];
20
+ hasEmbedding: boolean;
21
+ }
22
+ export interface MemorySearchOptions {
23
+ type?: MemoryType;
24
+ limit?: number;
25
+ minImportance?: number;
26
+ }
27
+ export interface MemoryStore {
28
+ /**
29
+ * Store a new memory
30
+ */
31
+ remember(content: string, options?: {
32
+ type?: MemoryType;
33
+ source?: string;
34
+ importance?: number;
35
+ tags?: string[];
36
+ }): Promise<string>;
37
+ /**
38
+ * Search memories using semantic similarity
39
+ */
40
+ search(query: string, options?: MemorySearchOptions): Promise<Memory[]>;
41
+ /**
42
+ * Get a specific memory by ID
43
+ */
44
+ get(id: string): Promise<Memory | null>;
45
+ /**
46
+ * Delete a memory
47
+ */
48
+ forget(id: string): Promise<boolean>;
49
+ /**
50
+ * Boost importance of a memory
51
+ */
52
+ reinforce(id: string, boost?: number): Promise<void>;
53
+ /**
54
+ * Get memory statistics
55
+ */
56
+ stats(): {
57
+ total: number;
58
+ byType: Record<string, number>;
59
+ avgImportance: number;
60
+ totalAssociations: number;
61
+ };
62
+ /**
63
+ * Close the store and release resources
64
+ */
65
+ close(): void | Promise<void>;
66
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Memory Store Interface
3
+ *
4
+ * Common interface for memory backends (SQLite, Postgres, etc.)
5
+ */
6
+ export {};
@@ -0,0 +1,109 @@
1
+ /**
2
+ * PostgreSQL Memory Store with pgvector
3
+ *
4
+ * Cloud-native memory backend for Railway deployments.
5
+ * Uses pgvector extension for efficient vector similarity search.
6
+ *
7
+ * Features:
8
+ * - All AdvancedMemoryStore features
9
+ * - Scalable Postgres backend
10
+ * - pgvector for vector similarity
11
+ * - Shared across multiple agent instances
12
+ */
13
+ export type MemoryType = 'episodic' | 'semantic' | 'procedural' | 'working';
14
+ export interface Memory {
15
+ id: string;
16
+ type: MemoryType;
17
+ content: string;
18
+ summary?: string;
19
+ source: string;
20
+ timestamp: string;
21
+ lastAccessed: string;
22
+ importance: number;
23
+ accessCount: number;
24
+ decayRate: number;
25
+ associations: string[];
26
+ tags: string[];
27
+ hasEmbedding: boolean;
28
+ }
29
+ interface PostgresMemoryConfig {
30
+ connectionString: string;
31
+ userId?: string;
32
+ dimensions?: number;
33
+ }
34
+ export declare class PostgresMemoryStore {
35
+ private pool;
36
+ private userId;
37
+ private dimensions;
38
+ private initialized;
39
+ constructor(config: PostgresMemoryConfig);
40
+ /**
41
+ * Initialize database schema
42
+ */
43
+ initialize(): Promise<void>;
44
+ /**
45
+ * Store a memory
46
+ */
47
+ remember(content: string, options?: {
48
+ type?: MemoryType;
49
+ source?: string;
50
+ importance?: number;
51
+ tags?: string[];
52
+ }): Promise<string>;
53
+ /**
54
+ * Search memories using vector similarity
55
+ */
56
+ search(query: string, options?: {
57
+ type?: MemoryType;
58
+ limit?: number;
59
+ minImportance?: number;
60
+ }): Promise<Memory[]>;
61
+ /**
62
+ * Get memory by ID
63
+ */
64
+ get(id: string): Promise<Memory | null>;
65
+ /**
66
+ * Delete a memory
67
+ */
68
+ forget(id: string): Promise<boolean>;
69
+ /**
70
+ * Update memory importance
71
+ */
72
+ reinforce(id: string, boost?: number): Promise<void>;
73
+ /**
74
+ * Get memory statistics
75
+ */
76
+ stats(): {
77
+ total: number;
78
+ byType: Record<string, number>;
79
+ avgImportance: number;
80
+ totalAssociations: number;
81
+ };
82
+ private _cachedStats;
83
+ /**
84
+ * Update stats cache (call periodically)
85
+ */
86
+ updateStatsCache(): Promise<void>;
87
+ /**
88
+ * Apply temporal decay to all memories
89
+ */
90
+ applyDecay(): Promise<void>;
91
+ /**
92
+ * Cleanup old low-importance memories
93
+ */
94
+ cleanup(options?: {
95
+ maxAge?: number;
96
+ minImportance?: number;
97
+ }): Promise<number>;
98
+ /**
99
+ * Close the connection pool
100
+ */
101
+ close(): Promise<void>;
102
+ private inferType;
103
+ private calculateImportance;
104
+ }
105
+ /**
106
+ * Check if Postgres is available and configured
107
+ */
108
+ export declare function isPostgresConfigured(): boolean;
109
+ export {};
@@ -0,0 +1,374 @@
1
+ /**
2
+ * PostgreSQL Memory Store with pgvector
3
+ *
4
+ * Cloud-native memory backend for Railway deployments.
5
+ * Uses pgvector extension for efficient vector similarity search.
6
+ *
7
+ * Features:
8
+ * - All AdvancedMemoryStore features
9
+ * - Scalable Postgres backend
10
+ * - pgvector for vector similarity
11
+ * - Shared across multiple agent instances
12
+ */
13
+ import pg from 'pg';
14
+ const { Pool } = pg;
15
+ // Simple local embedding using character n-grams (same as SQLite version)
16
+ function computeEmbedding(text, dimensions = 384) {
17
+ const embedding = new Array(dimensions).fill(0);
18
+ const normalized = text.toLowerCase();
19
+ // Character trigrams
20
+ for (let i = 0; i < normalized.length - 2; i++) {
21
+ const trigram = normalized.slice(i, i + 3);
22
+ const hash = hashString(trigram);
23
+ const idx = Math.abs(hash) % dimensions;
24
+ embedding[idx] += 1;
25
+ }
26
+ // Word unigrams
27
+ const words = normalized.split(/\s+/);
28
+ for (const word of words) {
29
+ const hash = hashString(word);
30
+ const idx = Math.abs(hash) % dimensions;
31
+ embedding[idx] += 2;
32
+ }
33
+ // Normalize
34
+ const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
35
+ if (magnitude > 0) {
36
+ for (let i = 0; i < embedding.length; i++) {
37
+ embedding[i] /= magnitude;
38
+ }
39
+ }
40
+ return embedding;
41
+ }
42
+ function hashString(str) {
43
+ let hash = 0;
44
+ for (let i = 0; i < str.length; i++) {
45
+ const char = str.charCodeAt(i);
46
+ hash = ((hash << 5) - hash) + char;
47
+ hash = hash & hash;
48
+ }
49
+ return hash;
50
+ }
51
+ export class PostgresMemoryStore {
52
+ pool;
53
+ userId;
54
+ dimensions;
55
+ initialized = false;
56
+ constructor(config) {
57
+ this.pool = new Pool({
58
+ connectionString: config.connectionString,
59
+ ssl: { rejectUnauthorized: false } // Required for Railway
60
+ });
61
+ this.userId = config.userId || 'default';
62
+ this.dimensions = config.dimensions || 384;
63
+ }
64
+ /**
65
+ * Initialize database schema
66
+ */
67
+ async initialize() {
68
+ if (this.initialized)
69
+ return;
70
+ const client = await this.pool.connect();
71
+ try {
72
+ // Enable pgvector extension
73
+ await client.query('CREATE EXTENSION IF NOT EXISTS vector');
74
+ // Create memories table
75
+ await client.query(`
76
+ CREATE TABLE IF NOT EXISTS memories (
77
+ id TEXT PRIMARY KEY,
78
+ user_id TEXT NOT NULL,
79
+ type TEXT NOT NULL,
80
+ content TEXT NOT NULL,
81
+ summary TEXT,
82
+ source TEXT,
83
+ timestamp TIMESTAMPTZ DEFAULT NOW(),
84
+ last_accessed TIMESTAMPTZ DEFAULT NOW(),
85
+ importance REAL DEFAULT 0.5,
86
+ access_count INTEGER DEFAULT 0,
87
+ decay_rate REAL DEFAULT 0.01,
88
+ associations TEXT[] DEFAULT '{}',
89
+ tags TEXT[] DEFAULT '{}',
90
+ embedding vector(${this.dimensions}),
91
+ created_at TIMESTAMPTZ DEFAULT NOW()
92
+ )
93
+ `);
94
+ // Create indexes
95
+ await client.query(`
96
+ CREATE INDEX IF NOT EXISTS idx_memories_user_id ON memories(user_id)
97
+ `);
98
+ await client.query(`
99
+ CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type)
100
+ `);
101
+ await client.query(`
102
+ CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance DESC)
103
+ `);
104
+ // Create vector similarity index (IVFFlat for performance)
105
+ await client.query(`
106
+ CREATE INDEX IF NOT EXISTS idx_memories_embedding
107
+ ON memories USING ivfflat (embedding vector_cosine_ops)
108
+ WITH (lists = 100)
109
+ `).catch(() => {
110
+ // IVFFlat requires at least 100 rows, fall back to exact search
111
+ console.log('[POSTGRES] IVFFlat index skipped (will use exact search)');
112
+ });
113
+ this.initialized = true;
114
+ console.log('[POSTGRES] Memory store initialized');
115
+ }
116
+ finally {
117
+ client.release();
118
+ }
119
+ }
120
+ /**
121
+ * Store a memory
122
+ */
123
+ async remember(content, options) {
124
+ await this.initialize();
125
+ const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
126
+ const type = options?.type || this.inferType(content);
127
+ const importance = options?.importance ?? this.calculateImportance(content);
128
+ const embedding = computeEmbedding(content, this.dimensions);
129
+ await this.pool.query(`
130
+ INSERT INTO memories (id, user_id, type, content, source, importance, tags, embedding)
131
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
132
+ `, [
133
+ id,
134
+ this.userId,
135
+ type,
136
+ content,
137
+ options?.source || 'user',
138
+ importance,
139
+ options?.tags || [],
140
+ `[${embedding.join(',')}]`
141
+ ]);
142
+ console.log(`[POSTGRES] Stored memory ${id} (${type}, importance=${importance.toFixed(2)})`);
143
+ return id;
144
+ }
145
+ /**
146
+ * Search memories using vector similarity
147
+ */
148
+ async search(query, options) {
149
+ await this.initialize();
150
+ const embedding = computeEmbedding(query, this.dimensions);
151
+ const limit = options?.limit || 10;
152
+ const minImportance = options?.minImportance || 0;
153
+ let sql = `
154
+ SELECT
155
+ id, type, content, summary, source, timestamp, last_accessed,
156
+ importance, access_count, decay_rate, associations, tags,
157
+ 1 - (embedding <=> $1::vector) as similarity
158
+ FROM memories
159
+ WHERE user_id = $2
160
+ AND importance >= $3
161
+ `;
162
+ const params = [
163
+ `[${embedding.join(',')}]`,
164
+ this.userId,
165
+ minImportance
166
+ ];
167
+ if (options?.type) {
168
+ sql += ` AND type = $${params.length + 1}`;
169
+ params.push(options.type);
170
+ }
171
+ sql += ` ORDER BY similarity DESC LIMIT $${params.length + 1}`;
172
+ params.push(limit);
173
+ const result = await this.pool.query(sql, params);
174
+ // Update access counts for retrieved memories
175
+ if (result.rows.length > 0) {
176
+ const ids = result.rows.map(r => r.id);
177
+ await this.pool.query(`
178
+ UPDATE memories
179
+ SET access_count = access_count + 1,
180
+ last_accessed = NOW()
181
+ WHERE id = ANY($1)
182
+ `, [ids]);
183
+ }
184
+ return result.rows.map(row => ({
185
+ id: row.id,
186
+ type: row.type,
187
+ content: row.content,
188
+ summary: row.summary,
189
+ source: row.source,
190
+ timestamp: row.timestamp.toISOString(),
191
+ lastAccessed: row.last_accessed.toISOString(),
192
+ importance: row.importance,
193
+ accessCount: row.access_count,
194
+ decayRate: row.decay_rate,
195
+ associations: row.associations || [],
196
+ tags: row.tags || [],
197
+ hasEmbedding: true
198
+ }));
199
+ }
200
+ /**
201
+ * Get memory by ID
202
+ */
203
+ async get(id) {
204
+ await this.initialize();
205
+ const result = await this.pool.query(`
206
+ SELECT * FROM memories WHERE id = $1 AND user_id = $2
207
+ `, [id, this.userId]);
208
+ if (result.rows.length === 0)
209
+ return null;
210
+ const row = result.rows[0];
211
+ return {
212
+ id: row.id,
213
+ type: row.type,
214
+ content: row.content,
215
+ summary: row.summary,
216
+ source: row.source,
217
+ timestamp: row.timestamp.toISOString(),
218
+ lastAccessed: row.last_accessed.toISOString(),
219
+ importance: row.importance,
220
+ accessCount: row.access_count,
221
+ decayRate: row.decay_rate,
222
+ associations: row.associations || [],
223
+ tags: row.tags || [],
224
+ hasEmbedding: true
225
+ };
226
+ }
227
+ /**
228
+ * Delete a memory
229
+ */
230
+ async forget(id) {
231
+ await this.initialize();
232
+ const result = await this.pool.query(`
233
+ DELETE FROM memories WHERE id = $1 AND user_id = $2
234
+ `, [id, this.userId]);
235
+ return (result.rowCount ?? 0) > 0;
236
+ }
237
+ /**
238
+ * Update memory importance
239
+ */
240
+ async reinforce(id, boost = 0.1) {
241
+ await this.initialize();
242
+ await this.pool.query(`
243
+ UPDATE memories
244
+ SET importance = LEAST(1.0, importance + $1)
245
+ WHERE id = $2 AND user_id = $3
246
+ `, [boost, id, this.userId]);
247
+ }
248
+ /**
249
+ * Get memory statistics
250
+ */
251
+ stats() {
252
+ // This is synchronous in the interface, but we need async for Postgres
253
+ // Return cached stats or default values
254
+ return this._cachedStats || {
255
+ total: 0,
256
+ byType: { episodic: 0, semantic: 0, procedural: 0, working: 0 },
257
+ avgImportance: 0.5,
258
+ totalAssociations: 0
259
+ };
260
+ }
261
+ _cachedStats = null;
262
+ /**
263
+ * Update stats cache (call periodically)
264
+ */
265
+ async updateStatsCache() {
266
+ await this.initialize();
267
+ const result = await this.pool.query(`
268
+ SELECT
269
+ COUNT(*) as total,
270
+ AVG(importance) as avg_importance,
271
+ SUM(array_length(associations, 1)) as total_associations
272
+ FROM memories
273
+ WHERE user_id = $1
274
+ `, [this.userId]);
275
+ const typeResult = await this.pool.query(`
276
+ SELECT type, COUNT(*) as count
277
+ FROM memories
278
+ WHERE user_id = $1
279
+ GROUP BY type
280
+ `, [this.userId]);
281
+ const byType = {
282
+ episodic: 0, semantic: 0, procedural: 0, working: 0
283
+ };
284
+ for (const row of typeResult.rows) {
285
+ byType[row.type] = parseInt(row.count);
286
+ }
287
+ this._cachedStats = {
288
+ total: parseInt(result.rows[0].total) || 0,
289
+ byType,
290
+ avgImportance: parseFloat(result.rows[0].avg_importance) || 0.5,
291
+ totalAssociations: parseInt(result.rows[0].total_associations) || 0
292
+ };
293
+ }
294
+ /**
295
+ * Apply temporal decay to all memories
296
+ */
297
+ async applyDecay() {
298
+ await this.initialize();
299
+ await this.pool.query(`
300
+ UPDATE memories
301
+ SET importance = GREATEST(0.1, importance * (1 - decay_rate))
302
+ WHERE user_id = $1
303
+ AND importance > 0.1
304
+ AND last_accessed < NOW() - INTERVAL '1 day'
305
+ `, [this.userId]);
306
+ }
307
+ /**
308
+ * Cleanup old low-importance memories
309
+ */
310
+ async cleanup(options) {
311
+ await this.initialize();
312
+ const maxAgeDays = (options?.maxAge || 30 * 24 * 60 * 60 * 1000) / (1000 * 60 * 60 * 24);
313
+ const minImportance = options?.minImportance || 0.2;
314
+ const result = await this.pool.query(`
315
+ DELETE FROM memories
316
+ WHERE user_id = $1
317
+ AND importance < $2
318
+ AND last_accessed < NOW() - INTERVAL '1 day' * $3
319
+ `, [this.userId, minImportance, maxAgeDays]);
320
+ const count = result.rowCount ?? 0;
321
+ if (count > 0) {
322
+ console.log(`[POSTGRES] Cleaned up ${count} old memories`);
323
+ }
324
+ return count;
325
+ }
326
+ /**
327
+ * Close the connection pool
328
+ */
329
+ async close() {
330
+ await this.pool.end();
331
+ console.log('[POSTGRES] Connection pool closed');
332
+ }
333
+ // Helper methods
334
+ inferType(content) {
335
+ const lower = content.toLowerCase();
336
+ // Procedural indicators
337
+ if (lower.includes('how to') || lower.includes('steps to') ||
338
+ lower.includes('process for') || lower.includes('when doing')) {
339
+ return 'procedural';
340
+ }
341
+ // Episodic indicators (past tense, specific events)
342
+ if (lower.includes('yesterday') || lower.includes('last week') ||
343
+ lower.includes('we did') || lower.includes('happened')) {
344
+ return 'episodic';
345
+ }
346
+ // Default to semantic (facts)
347
+ return 'semantic';
348
+ }
349
+ calculateImportance(content) {
350
+ let importance = 0.5;
351
+ // Boost for longer, detailed content
352
+ if (content.length > 200)
353
+ importance += 0.1;
354
+ if (content.length > 500)
355
+ importance += 0.1;
356
+ // Boost for structured content
357
+ if (content.includes(':') || content.includes('-') || content.includes('•')) {
358
+ importance += 0.1;
359
+ }
360
+ // Boost for preference indicators
361
+ const lower = content.toLowerCase();
362
+ if (lower.includes('prefer') || lower.includes('always') ||
363
+ lower.includes('never') || lower.includes('important')) {
364
+ importance += 0.15;
365
+ }
366
+ return Math.min(1.0, importance);
367
+ }
368
+ }
369
+ /**
370
+ * Check if Postgres is available and configured
371
+ */
372
+ export function isPostgresConfigured() {
373
+ return !!process.env.DATABASE_URL;
374
+ }
package/dist/heartbeat.js CHANGED
@@ -10,7 +10,7 @@ import { hostname } from 'os';
10
10
  import { createHash } from 'crypto';
11
11
  import { getServerUrl, getAgentToken, getUserId, isCloudMode } from './config.js';
12
12
  const HEARTBEAT_INTERVAL = 10000; // 10 seconds
13
- const VERSION = '0.2.10'; // Should match package.json
13
+ const VERSION = '0.2.11'; // Should match package.json
14
14
  const state = {
15
15
  intervalId: null,
16
16
  runnerId: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -43,6 +43,7 @@
43
43
  "conf": "^13.0.1",
44
44
  "node-cron": "^3.0.3",
45
45
  "ora": "^8.1.1",
46
+ "pg": "^8.13.1",
46
47
  "vectra": "^0.9.0",
47
48
  "ws": "^8.18.3"
48
49
  },
@@ -50,6 +51,7 @@
50
51
  "@types/better-sqlite3": "^7.6.12",
51
52
  "@types/node": "^22.10.2",
52
53
  "@types/node-cron": "^3.0.11",
54
+ "@types/pg": "^8.11.10",
53
55
  "@types/ws": "^8.18.1",
54
56
  "typescript": "^5.7.2"
55
57
  }