persyst-mcp 2.2.4 → 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
@@ -246,7 +246,7 @@ function jaccardSimilarity(a, b) {
246
246
  * @param {string|null} agentId - Querying agent identifier
247
247
  * @param {string|null} sessionId - Current session ID
248
248
  */
249
- 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) {
250
250
  // Extract entities mentioned in the query text to seed the graph search directly
251
251
  const entities = getAllEntities(100);
252
252
  const matchedEntityIds = new Set();
@@ -423,8 +423,23 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
423
423
  accepted.push(c);
424
424
  }
425
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
+
426
430
  // 6. Format LLM injection context string
427
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';
428
443
  if (accepted.length === 0) {
429
444
  context += 'No relevant memories retrieved.\n';
430
445
  } else {
@@ -447,7 +462,10 @@ export async function getOptimizedContext(queryText, maxTokens, agentId = null,
447
462
  return {
448
463
  context,
449
464
  memories: accepted,
450
- attestation
465
+ attestation,
466
+ intent,
467
+ urgency,
468
+ suggested_actions
451
469
  };
452
470
  }
453
471
 
@@ -631,3 +649,75 @@ export async function consolidateMemories(namespace = null) {
631
649
  details: consolidated
632
650
  };
633
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
+ }