persyst-mcp 2.2.3 → 2.2.5

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/src/sdk.d.ts ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * TypeScript type declarations for the Persyst Developer SDK.
3
+ * Import via: import { Persyst } from 'persyst-mcp/sdk'
4
+ */
5
+
6
+ export interface PersystConfig {
7
+ /**
8
+ * Force a connection mode. If omitted, Persyst auto-detects.
9
+ * - `'gateway'` — connect to the local HTTP gateway on port 4321
10
+ * - `'library'` — use in-process SQLite directly (no server needed)
11
+ * - `null` — auto-detect: probe gateway first, fall back to library
12
+ */
13
+ mode?: 'gateway' | 'library' | null;
14
+ /** Gateway host (default: '127.0.0.1') */
15
+ host?: string;
16
+ /** Gateway port (default: 4321) */
17
+ port?: number;
18
+ /** Optional API key for gateway authorization */
19
+ apiKey?: string | null;
20
+ }
21
+
22
+ export interface TrackOptions {
23
+ /** Active session or thread identifier */
24
+ sessionId?: string;
25
+ /** @alias sessionId */
26
+ session_id?: string;
27
+ /**
28
+ * Active workflow name (used as agent_id for namespace isolation).
29
+ * Example: 'customer_support', 'code_review'
30
+ */
31
+ workflow?: string;
32
+ /**
33
+ * Specific event name. Used to build `content` if `content` is not provided.
34
+ * Example: 'payment_failed', 'build_passed'
35
+ */
36
+ event?: string;
37
+ /** Full text content to store as the memory. Required if `event` is not provided. */
38
+ content?: string;
39
+ /** Structured metadata to append to the generated content string. */
40
+ metadata?: Record<string, unknown>;
41
+ /** Importance score from 0.0 (low) to 1.0 (high). Default: 1.0 */
42
+ importance?: number;
43
+ /**
44
+ * If true (default), the memory is visible to all agents.
45
+ * If false, it is isolated to the `workflow` agent's namespace.
46
+ */
47
+ shared?: boolean;
48
+ }
49
+
50
+ export interface TrackResult {
51
+ success: boolean;
52
+ /** The ID of the stored (or existing) memory */
53
+ id: number;
54
+ /** The namespace the memory was written to */
55
+ namespace: string;
56
+ /** Human-readable result message */
57
+ message?: string;
58
+ /** Error message, present only on failure */
59
+ error?: string;
60
+ }
61
+
62
+ export interface ProvenanceRecord {
63
+ source_type: 'agent' | 'git' | 'api' | 'import' | 'manual';
64
+ source_id: string | null;
65
+ confidence: number;
66
+ }
67
+
68
+ export interface MemoryRecord {
69
+ id: number;
70
+ content: string;
71
+ importance_score: number;
72
+ created_at: number;
73
+ last_accessed: number;
74
+ /** Relevance score (hybrid search + reputation weight) */
75
+ score: number;
76
+ provenance?: ProvenanceRecord | null;
77
+ }
78
+
79
+ export interface ContextOptions {
80
+ /** Active session or thread identifier */
81
+ sessionId?: string;
82
+ /** @alias sessionId */
83
+ session_id?: string;
84
+ /** Active workflow / agent name */
85
+ workflow?: string;
86
+ /**
87
+ * Hint for the active task intent. Used to refine context selection.
88
+ * Example: 'debugging', 'ui_styling', 'database_management', 'deployment'
89
+ */
90
+ intent?: string;
91
+ /** Search query string — required */
92
+ query: string;
93
+ /** Hard token budget for the returned context block. Default: 2000 */
94
+ maxTokens?: number;
95
+ /** @alias maxTokens */
96
+ max_tokens?: number;
97
+ }
98
+
99
+ export interface ContextResult {
100
+ /**
101
+ * A formatted, ready-to-inject context string for LLM system prompts.
102
+ * Contains memory entries ranked by relevance within the token budget.
103
+ */
104
+ context: string;
105
+ /** The ranked memory records included in the context */
106
+ memories: MemoryRecord[];
107
+ /** Cryptographic Ed25519 attestation record for audit trails */
108
+ attestation: object | null;
109
+ /** Detected query intent classification */
110
+ intent: string;
111
+ /** Detected urgency level based on query language */
112
+ urgency: 'low' | 'medium' | 'high' | 'critical';
113
+ /** Generated actionable suggestions derived from retrieved memories */
114
+ suggested_actions: string[];
115
+ }
116
+
117
+ /**
118
+ * Persyst Developer SDK Client.
119
+ *
120
+ * Supports two transport modes:
121
+ * - **Gateway Mode**: communicates with the local HTTP gateway on port 4321.
122
+ * Best for Python/other-language agents, or when the server is already running.
123
+ * - **Library Mode**: directly accesses the local SQLite database in-process.
124
+ * Best for Node.js scripts and when no server is needed.
125
+ *
126
+ * Auto-detects the available mode on first call (150ms probe timeout).
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * import { Persyst } from 'persyst-mcp/sdk';
131
+ *
132
+ * const persyst = new Persyst();
133
+ *
134
+ * // Track an architectural decision
135
+ * await persyst.track({
136
+ * content: 'We use TypeScript for all new source files',
137
+ * importance: 0.9,
138
+ * workflow: 'my-agent'
139
+ * });
140
+ *
141
+ * // Retrieve compressed context for an LLM
142
+ * const { context } = await persyst.context({
143
+ * query: 'coding conventions and stack choices',
144
+ * intent: 'general'
145
+ * });
146
+ * console.log(context);
147
+ * ```
148
+ */
149
+ export declare class Persyst {
150
+ constructor(config?: PersystConfig);
151
+
152
+ /**
153
+ * Track a developer event or milestone.
154
+ *
155
+ * Stores it as a persistent memory in the local SQLite database via
156
+ * the gateway (HTTP) or directly (library mode).
157
+ *
158
+ * If an identical memory already exists, its importance is boosted instead
159
+ * of creating a duplicate.
160
+ *
161
+ * @throws {Error} If neither `content` nor `event` is provided.
162
+ */
163
+ track(eventData: TrackOptions): Promise<TrackResult>;
164
+
165
+ /**
166
+ * Retrieve compiled, optimized context tailored by query and intent.
167
+ *
168
+ * Runs hybrid search (keyword + semantic) + knowledge graph traversal,
169
+ * applies temporal decay + agent reputation weighting, then compresses
170
+ * the result to fit within `maxTokens`.
171
+ *
172
+ * Returns a formatted context block ready to inject into an LLM system prompt.
173
+ */
174
+ context(contextQuery: ContextOptions): Promise<ContextResult>;
175
+ }
package/src/sdk.js ADDED
@@ -0,0 +1,217 @@
1
+ import http from 'http';
2
+ import net from 'net';
3
+
4
+ /**
5
+ * Persyst Developer SDK Client
6
+ * Supports both Gateway Mode (local HTTP server) and Library Mode (direct in-process SQLite).
7
+ */
8
+ export class Persyst {
9
+ /**
10
+ * @param {Object} [config={}]
11
+ * @param {string} [config.mode=null] - 'gateway' | 'library' | null (auto-detect)
12
+ * @param {string} [config.host='127.0.0.1'] - Gateway host
13
+ * @param {number} [config.port=4321] - Gateway port
14
+ * @param {string} [config.apiKey=null] - Gateway authorization key
15
+ */
16
+ constructor(config = {}) {
17
+ this.mode = config.mode || null;
18
+ this.host = config.host || '127.0.0.1';
19
+ this.port = config.port || 4321;
20
+ this.apiKey = config.apiKey || null;
21
+ this._detectedMode = null;
22
+ }
23
+
24
+ /**
25
+ * Auto-detect reachable Gateway on port 4321, falling back to direct library mode.
26
+ * @private
27
+ */
28
+ async _resolveMode() {
29
+ if (this.mode) {
30
+ return this.mode;
31
+ }
32
+ if (this._detectedMode) {
33
+ return this._detectedMode;
34
+ }
35
+
36
+ try {
37
+ const isReachable = await new Promise((resolve) => {
38
+ const socket = new net.Socket();
39
+ socket.setTimeout(150); // 150ms probe timeout
40
+ socket.on('connect', () => {
41
+ socket.destroy();
42
+ resolve(true);
43
+ });
44
+ socket.on('error', () => {
45
+ socket.destroy();
46
+ resolve(false);
47
+ });
48
+ socket.on('timeout', () => {
49
+ socket.destroy();
50
+ resolve(false);
51
+ });
52
+ socket.connect(this.port, this.host);
53
+ });
54
+ this._detectedMode = isReachable ? 'gateway' : 'library';
55
+ } catch (_) {
56
+ this._detectedMode = 'library';
57
+ }
58
+ return this._detectedMode;
59
+ }
60
+
61
+ /**
62
+ * Track a developer event or milestone.
63
+ * @param {Object} eventData
64
+ * @param {string} [eventData.sessionId] - Active session / thread identifier
65
+ * @param {string} [eventData.workflow] - Active workflow name (e.g. 'customer_support')
66
+ * @param {string} [eventData.event] - Specific event name (e.g. 'payment_failed')
67
+ * @param {string} [eventData.content] - Full text detail of the event
68
+ * @param {Object} [eventData.metadata] - Structured metadata facts
69
+ * @param {number} [eventData.importance=1.0] - Importance score (0.0 - 1.0)
70
+ * @param {boolean} [eventData.shared=true] - Whether the memory is shared across namespaces
71
+ */
72
+ async track(eventData = {}) {
73
+ const mode = await this._resolveMode();
74
+ const sessionId = eventData.sessionId || eventData.session_id || null;
75
+ const workflow = eventData.workflow || null;
76
+ const event = eventData.event || null;
77
+ const metadata = eventData.metadata || null;
78
+ const importance = eventData.importance !== undefined ? eventData.importance : 1.0;
79
+ const shared = eventData.shared !== undefined ? eventData.shared : true;
80
+
81
+ let content = eventData.content || '';
82
+ if (!content) {
83
+ if (event) {
84
+ content = `Event: ${event}`;
85
+ if (workflow) content += ` in workflow: ${workflow}`;
86
+ if (metadata) content += `. Metadata: ${JSON.stringify(metadata)}`;
87
+ } else {
88
+ throw new Error('Either content or event must be provided to track()');
89
+ }
90
+ }
91
+
92
+ if (mode === 'gateway') {
93
+ return this._trackGateway({ content, importance, agent_id: workflow || 'sdk', session_id: sessionId, shared });
94
+ } else {
95
+ return this._trackLibrary({ content, importance, agent_id: workflow || 'sdk', session_id: sessionId, shared });
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Internal direct SQLite write.
101
+ * @private
102
+ */
103
+ async _trackLibrary({ content, importance, agent_id, session_id, shared }) {
104
+ const { insertMemory, insertVector } = await import('./database.js');
105
+ const { generateEmbedding } = await import('./embeddings.js');
106
+
107
+ const namespace = shared ? 'shared' : agent_id;
108
+ const id = insertMemory(content, importance, {
109
+ source_type: 'api',
110
+ source_id: agent_id,
111
+ confidence: 1.0
112
+ }, namespace);
113
+
114
+ const embedding = await generateEmbedding(content);
115
+ insertVector(id, embedding);
116
+ return { success: true, id };
117
+ }
118
+
119
+ /**
120
+ * Internal HTTP POST write to Gateway.
121
+ * @private
122
+ */
123
+ _trackGateway(payload) {
124
+ return new Promise((resolve, reject) => {
125
+ const body = JSON.stringify(payload);
126
+ const req = http.request({
127
+ hostname: this.host,
128
+ port: this.port,
129
+ path: '/add',
130
+ method: 'POST',
131
+ headers: {
132
+ 'Content-Type': 'application/json',
133
+ 'Content-Length': Buffer.byteLength(body)
134
+ }
135
+ }, (res) => {
136
+ let responseBody = '';
137
+ res.on('data', chunk => { responseBody += chunk; });
138
+ res.on('end', () => {
139
+ try {
140
+ resolve(JSON.parse(responseBody));
141
+ } catch (e) {
142
+ reject(new Error(`Failed to parse gateway response: ${e.message}`));
143
+ }
144
+ });
145
+ });
146
+ req.on('error', reject);
147
+ req.write(body);
148
+ req.end();
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Retrieve compiled, optimized context tailored by intent and workflow.
154
+ * @param {Object} contextQuery
155
+ * @param {string} [contextQuery.sessionId] - Active session / thread identifier
156
+ * @param {string} [contextQuery.workflow] - Active workflow name (e.g. 'customer_support')
157
+ * @param {string} [contextQuery.intent] - Active reasoning intent (e.g. 'debugging')
158
+ * @param {string} contextQuery.query - Prompt query string to find similar context for
159
+ * @param {number} [contextQuery.maxTokens=2000] - Hard budget limit of tokens
160
+ */
161
+ async context(contextQuery = {}) {
162
+ const mode = await this._resolveMode();
163
+ const sessionId = contextQuery.sessionId || contextQuery.session_id || null;
164
+ const workflow = contextQuery.workflow || null;
165
+ const intent = contextQuery.intent || null;
166
+ const query = contextQuery.query || '';
167
+ const maxTokens = contextQuery.maxTokens || contextQuery.max_tokens || 2000;
168
+
169
+ if (mode === 'gateway') {
170
+ return this._contextGateway({ query, max_tokens: maxTokens, agent_id: workflow, session_id: sessionId, intent });
171
+ } else {
172
+ return this._contextLibrary({ query, max_tokens: maxTokens, agent_id: workflow, session_id: sessionId, intent });
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Internal direct SQLite read.
178
+ * @private
179
+ */
180
+ async _contextLibrary({ query, max_tokens, agent_id, session_id, intent }) {
181
+ const { getOptimizedContext } = await import('./search.js');
182
+ return getOptimizedContext(query, max_tokens, agent_id, session_id, agent_id || null, intent);
183
+ }
184
+
185
+ /**
186
+ * Internal HTTP POST read from Gateway.
187
+ * @private
188
+ */
189
+ _contextGateway(payload) {
190
+ return new Promise((resolve, reject) => {
191
+ const body = JSON.stringify(payload);
192
+ const req = http.request({
193
+ hostname: this.host,
194
+ port: this.port,
195
+ path: '/context',
196
+ method: 'POST',
197
+ headers: {
198
+ 'Content-Type': 'application/json',
199
+ 'Content-Length': Buffer.byteLength(body)
200
+ }
201
+ }, (res) => {
202
+ let responseBody = '';
203
+ res.on('data', chunk => { responseBody += chunk; });
204
+ res.on('end', () => {
205
+ try {
206
+ resolve(JSON.parse(responseBody));
207
+ } catch (e) {
208
+ reject(new Error(`Failed to parse gateway response: ${e.message}`));
209
+ }
210
+ });
211
+ });
212
+ req.on('error', reject);
213
+ req.write(body);
214
+ req.end();
215
+ });
216
+ }
217
+ }
package/src/search.js CHANGED
@@ -33,6 +33,12 @@ let lastDataVersion = 0;
33
33
  * @returns {Promise<Array>} Ranked search results (with .attestation property attached)
34
34
  */
35
35
  export async function searchHybrid(queryText, limit = 5, agentId = null, sessionId = null, namespace = null, skipAttestation = false) {
36
+ if (typeof limit !== 'number' || isNaN(limit) || limit <= 0) {
37
+ throw new Error('Limit must be a positive integer.');
38
+ }
39
+ const parsedLimit = Math.floor(limit);
40
+ const ns = namespace || 'shared';
41
+
36
42
  // Sync in-memory cache with external DB changes using sqlite data_version
37
43
  try {
38
44
  const currentDataVersion = db.pragma('data_version', { simple: true });
@@ -46,7 +52,7 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
46
52
 
47
53
  // --- Check LRU cache first (Feature 1) ---
48
54
  // Include namespace in cache key to prevent cross-namespace cache hits
49
- const cacheKey = LRUCache.key(`${namespace || 'all'}:${queryText}`, limit);
55
+ const cacheKey = LRUCache.key(`${ns}:${queryText}`, parsedLimit);
50
56
  const cached = searchCache.get(cacheKey);
51
57
  if (cached) {
52
58
  console.error(`[persyst-cache] Cache HIT for query: "${queryText.slice(0, 50)}..."`);
@@ -54,12 +60,12 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
54
60
  }
55
61
 
56
62
  // --- Step 1: Keyword search (fast, exact matches) ---
57
- const keywordHits = searchKeyword(queryText, limit * 2);
63
+ const keywordHits = searchKeyword(queryText, parsedLimit * 2);
58
64
  const keywordIds = new Set(keywordHits.map(r => r.id));
59
65
 
60
66
  // --- Step 2: Semantic search (meaning-based) ---
61
67
  const queryEmbedding = await generateEmbedding(queryText);
62
- const vecHits = searchVector(queryEmbedding, limit * 2);
68
+ const vecHits = searchVector(queryEmbedding, parsedLimit * 2);
63
69
 
64
70
  const semanticResults = vecHits.map(r => ({
65
71
  id: r.rowid,
@@ -99,7 +105,7 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
99
105
  const finalResults = combined
100
106
  .map(r => {
101
107
  // Use namespace-aware getMemoryById to filter by agent namespace
102
- const memory = getMemoryById(r.id, namespace);
108
+ const memory = getMemoryById(r.id, ns);
103
109
  if (!memory) return null; // Memory was archived, deleted, or not in namespace
104
110
 
105
111
  // Boost memory access metrics
@@ -141,7 +147,7 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
141
147
  finalResults.sort((a, b) => parseFloat(b.hybrid_score) - parseFloat(a.hybrid_score));
142
148
 
143
149
  // --- Step 5: Apply MMR for diverse retrieval (Feature 3) ---
144
- const mmrResults = applyMMR(finalResults, limit);
150
+ const mmrResults = applyMMR(finalResults, parsedLimit);
145
151
 
146
152
  // Generate cryptographic attestation for audit trails (skip if called internally)
147
153
  let attestation = null;
@@ -240,7 +246,7 @@ function jaccardSimilarity(a, b) {
240
246
  * @param {string|null} agentId - Querying agent identifier
241
247
  * @param {string|null} sessionId - Current session ID
242
248
  */
243
- export async function getOptimizedContext(queryText, maxTokens, agentId = null, sessionId = null, namespace = null) {
249
+ export async function getOptimizedContext(queryText, maxTokens, agentId = null, sessionId = null, namespace = null, intentParam = null) {
244
250
  // Extract entities mentioned in the query text to seed the graph search directly
245
251
  const entities = getAllEntities(100);
246
252
  const matchedEntityIds = new Set();
@@ -417,8 +423,23 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
417
423
  accepted.push(c);
418
424
  }
419
425
 
426
+ // Classify intent and urgency based on query text and parameters
427
+ const { intent, urgency } = classifyIntentAndUrgency(queryText, intentParam);
428
+ const suggested_actions = generateSuggestedActions(accepted, intent, urgency);
429
+
420
430
  // 6. Format LLM injection context string
421
431
  let context = '=== RETRIEVED AGENT MEMORY CONTEXT ===\n';
432
+ context += `[Intent: ${intent} | Urgency: ${urgency}]\n\n`;
433
+
434
+ if (suggested_actions.length > 0) {
435
+ context += '[Suggested Actions]\n';
436
+ for (const action of suggested_actions) {
437
+ context += `• ${action}\n`;
438
+ }
439
+ context += '\n';
440
+ }
441
+
442
+ context += '[Memories]\n';
422
443
  if (accepted.length === 0) {
423
444
  context += 'No relevant memories retrieved.\n';
424
445
  } else {
@@ -441,7 +462,10 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
441
462
  return {
442
463
  context,
443
464
  memories: accepted,
444
- attestation
465
+ attestation,
466
+ intent,
467
+ urgency,
468
+ suggested_actions
445
469
  };
446
470
  }
447
471
 
@@ -625,3 +649,75 @@ export async function consolidateMemories(namespace = null) {
625
649
  details: consolidated
626
650
  };
627
651
  }
652
+
653
+ /**
654
+ * Classify context retrieval intent and urgency level using heuristic analysis.
655
+ */
656
+ function classifyIntentAndUrgency(queryText, intentParam = null) {
657
+ const queryLower = (queryText || '').toLowerCase();
658
+
659
+ // 1. Determine Intent
660
+ let intent = intentParam || 'general';
661
+ if (intent === 'general' || !intent) {
662
+ if (/(?:db|database|sqlite|sql|table|migration|schema)/i.test(queryLower)) {
663
+ intent = 'database_management';
664
+ } else if (/(?:deploy|ci|cd|vercel|publish|release|prod|staging)/i.test(queryLower)) {
665
+ intent = 'deployment';
666
+ } else if (/(?:style|css|html|theme|design|layout|align|color|font)/i.test(queryLower)) {
667
+ intent = 'ui_styling';
668
+ } else if (/(?:test|spec|unit|mock|heavy|smoke)/i.test(queryLower)) {
669
+ intent = 'testing';
670
+ } else if (/(?:error|bug|fail|crash|break|exception|stack|trace|refused|debug)/i.test(queryLower)) {
671
+ intent = 'debugging';
672
+ }
673
+ }
674
+
675
+ // 2. Determine Urgency
676
+ let urgency = 'low';
677
+ if (/(?:panic|emergency|broken|critical|urgent|fatal|security|leak|bypass|vulnerability)/i.test(queryLower)) {
678
+ urgency = 'critical';
679
+ } else if (/(?:fail|error|crash|prevent|stop|warn|warning|issue|broken)/i.test(queryLower)) {
680
+ urgency = 'high';
681
+ } else if (/(?:update|change|add|tweak|check|verify)/i.test(queryLower)) {
682
+ urgency = 'medium';
683
+ }
684
+
685
+ return { intent, urgency };
686
+ }
687
+
688
+ /**
689
+ * Generate actionable suggested actions based on active memories and query classification.
690
+ */
691
+ function generateSuggestedActions(memories, intent, urgency) {
692
+ const actions = [];
693
+
694
+ // General recommendation based on intent
695
+ if (intent === 'debugging') {
696
+ actions.push('Inspect the recent error logs and verify SQLite/system constraints.');
697
+ } else if (intent === 'ui_styling') {
698
+ actions.push('Verify UI layouts conform to user design preferences.');
699
+ } else if (intent === 'database_management') {
700
+ actions.push('Ensure database migrations are applied and referential integrity is checked.');
701
+ }
702
+
703
+ for (const m of memories) {
704
+ const content = m.content.toLowerCase();
705
+
706
+ // Check for rules/decisions in memory content
707
+ if (content.includes('decision:') || content.includes('rule:')) {
708
+ actions.push(`Adhere to guideline: ${m.content.slice(0, 100)}...`);
709
+ } else if (content.includes('prefer')) {
710
+ actions.push(`Apply user preference: ${m.content.slice(0, 100)}...`);
711
+ } else if (content.includes('error') || content.includes('bug') || content.includes('fix')) {
712
+ actions.push(`Reference past fix: ${m.content.slice(0, 100)}...`);
713
+ }
714
+ }
715
+
716
+ // Safety guideline if critical
717
+ if (urgency === 'critical') {
718
+ actions.unshift('CAUTION: Address security, vulnerability, or critical stability factors immediately.');
719
+ }
720
+
721
+ // Deduplicate
722
+ return Array.from(new Set(actions));
723
+ }