persyst-mcp 2.2.4 → 2.2.6
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/README.md +64 -2
- package/bin/export.js +116 -0
- package/bin/import.js +160 -0
- package/bin/init.js +168 -32
- package/bin/mcp.js +7 -0
- package/hooks/persyst-hook.js +9 -10
- package/index.js +42 -12
- package/package.json +15 -10
- package/src/attestation.js +49 -28
- package/src/database.js +229 -36
- package/src/events.js +19 -0
- package/src/extractor-heuristic.js +505 -324
- package/src/sdk.d.ts +175 -0
- package/src/sdk.js +218 -0
- package/src/search.js +144 -83
- package/src/server.js +766 -93
- package/src/setup-wasm.js +34 -39
- package/src/text-utils.js +41 -0
- package/src/tools.js +58 -46
- package/src/watcher.js +174 -50
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,218 @@
|
|
|
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, redactSecrets } = await import('./database.js');
|
|
105
|
+
const { generateEmbedding } = await import('./embeddings.js');
|
|
106
|
+
|
|
107
|
+
const namespace = shared ? 'shared' : agent_id;
|
|
108
|
+
const redactedContent = redactSecrets ? redactSecrets(content) : content;
|
|
109
|
+
const id = insertMemory(redactedContent, importance, {
|
|
110
|
+
source_type: 'api',
|
|
111
|
+
source_id: agent_id,
|
|
112
|
+
confidence: 1.0
|
|
113
|
+
}, namespace);
|
|
114
|
+
|
|
115
|
+
const embedding = await generateEmbedding(redactedContent);
|
|
116
|
+
insertVector(id, embedding);
|
|
117
|
+
return { success: true, id };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Internal HTTP POST write to Gateway.
|
|
122
|
+
* @private
|
|
123
|
+
*/
|
|
124
|
+
_trackGateway(payload) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const body = JSON.stringify(payload);
|
|
127
|
+
const req = http.request({
|
|
128
|
+
hostname: this.host,
|
|
129
|
+
port: this.port,
|
|
130
|
+
path: '/add',
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: {
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
'Content-Length': Buffer.byteLength(body)
|
|
135
|
+
}
|
|
136
|
+
}, (res) => {
|
|
137
|
+
let responseBody = '';
|
|
138
|
+
res.on('data', chunk => { responseBody += chunk; });
|
|
139
|
+
res.on('end', () => {
|
|
140
|
+
try {
|
|
141
|
+
resolve(JSON.parse(responseBody));
|
|
142
|
+
} catch (e) {
|
|
143
|
+
reject(new Error(`Failed to parse gateway response: ${e.message}`));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
req.on('error', reject);
|
|
148
|
+
req.write(body);
|
|
149
|
+
req.end();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Retrieve compiled, optimized context tailored by intent and workflow.
|
|
155
|
+
* @param {Object} contextQuery
|
|
156
|
+
* @param {string} [contextQuery.sessionId] - Active session / thread identifier
|
|
157
|
+
* @param {string} [contextQuery.workflow] - Active workflow name (e.g. 'customer_support')
|
|
158
|
+
* @param {string} [contextQuery.intent] - Active reasoning intent (e.g. 'debugging')
|
|
159
|
+
* @param {string} contextQuery.query - Prompt query string to find similar context for
|
|
160
|
+
* @param {number} [contextQuery.maxTokens=2000] - Hard budget limit of tokens
|
|
161
|
+
*/
|
|
162
|
+
async context(contextQuery = {}) {
|
|
163
|
+
const mode = await this._resolveMode();
|
|
164
|
+
const sessionId = contextQuery.sessionId || contextQuery.session_id || null;
|
|
165
|
+
const workflow = contextQuery.workflow || null;
|
|
166
|
+
const intent = contextQuery.intent || null;
|
|
167
|
+
const query = contextQuery.query || '';
|
|
168
|
+
const maxTokens = contextQuery.maxTokens || contextQuery.max_tokens || 2000;
|
|
169
|
+
|
|
170
|
+
if (mode === 'gateway') {
|
|
171
|
+
return this._contextGateway({ query, max_tokens: maxTokens, agent_id: workflow, session_id: sessionId, intent });
|
|
172
|
+
} else {
|
|
173
|
+
return this._contextLibrary({ query, max_tokens: maxTokens, agent_id: workflow, session_id: sessionId, intent });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Internal direct SQLite read.
|
|
179
|
+
* @private
|
|
180
|
+
*/
|
|
181
|
+
async _contextLibrary({ query, max_tokens, agent_id, session_id, intent }) {
|
|
182
|
+
const { getOptimizedContext } = await import('./search.js');
|
|
183
|
+
return getOptimizedContext(query, max_tokens, agent_id, session_id, agent_id || null, intent);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Internal HTTP POST read from Gateway.
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
_contextGateway(payload) {
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
const body = JSON.stringify(payload);
|
|
193
|
+
const req = http.request({
|
|
194
|
+
hostname: this.host,
|
|
195
|
+
port: this.port,
|
|
196
|
+
path: '/context',
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: {
|
|
199
|
+
'Content-Type': 'application/json',
|
|
200
|
+
'Content-Length': Buffer.byteLength(body)
|
|
201
|
+
}
|
|
202
|
+
}, (res) => {
|
|
203
|
+
let responseBody = '';
|
|
204
|
+
res.on('data', chunk => { responseBody += chunk; });
|
|
205
|
+
res.on('end', () => {
|
|
206
|
+
try {
|
|
207
|
+
resolve(JSON.parse(responseBody));
|
|
208
|
+
} catch (e) {
|
|
209
|
+
reject(new Error(`Failed to parse gateway response: ${e.message}`));
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
req.on('error', reject);
|
|
214
|
+
req.write(body);
|
|
215
|
+
req.end();
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|