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/README.md
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
# Persyst
|
|
2
2
|
|
|
3
|
-
**Local-first MCP memory
|
|
3
|
+
**Local-first, compliance-grade MCP memory layer for regulated enterprise coding teams using AI assistants.**
|
|
4
4
|
|
|
5
|
-
Persyst gives AI coding agents (Claude Code, Cursor, VS Code, Aider,
|
|
5
|
+
Persyst gives AI coding agents (Claude Code, Cursor, VS Code, Aider, Continue.dev, Antigravity) persistent memory across sessions. It stores memories in a local SQLite database with hybrid keyword + semantic search — operating 100% offline with zero cloud egress.
|
|
6
|
+
|
|
7
|
+
## Compliance-Grade Security Features
|
|
8
|
+
|
|
9
|
+
Persyst is built from the ground up for highly regulated enterprise environments (finance, healthcare, defense) subject to **SOC 2**, **HIPAA**, and the **EU AI Act**:
|
|
10
|
+
|
|
11
|
+
* **100% Data Residency (Zero-Egress)**: All vector calculations, full-text searches, and model inferences run locally on the developer's workstation. No database records or context data ever leave the local machine. Bypasses Business Associate Agreement (BAA) complexity for HIPAA.
|
|
12
|
+
* **Cryptographic Chain of Custody**: Every context retrieval generates an Ed25519 cryptographic signature sealing the query and retrieved memory hashes. Each attestation is chained to the previous one via SHA-256 hash chains, creating a tamper-evident audit ledger verifiable by security teams.
|
|
13
|
+
* **Automatic Secret Redaction**: Scans incoming log files and text writes to redact high-entropy secrets (API keys, JWTs, database strings, private keys) before they reach the persistent database.
|
|
14
|
+
* **Reactive File Watching**: Integrates `chokidar` for instant event-driven scanning of agent transcript folders, guaranteeing that your memories are synchronized immediately after each agent interaction.
|
|
15
|
+
|
|
16
|
+
*Read more in our compliance mapping guides:*
|
|
17
|
+
- [SOC 2 Type II Controls](file:///c:/Users/Super/Desktop/Peryst/compliance/SOC2-controls.md)
|
|
18
|
+
- [HIPAA Mapping & PHI Boundaries](file:///c:/Users/Super/Desktop/Peryst/compliance/HIPAA-mapping.md)
|
|
19
|
+
- [EU AI Act Article 13 Transparency](file:///c:/Users/Super/Desktop/Peryst/compliance/EU-AI-Act-Article13.md)
|
|
20
|
+
- [Compliance Audit Trail Sample](file:///c:/Users/Super/Desktop/Peryst/compliance/audit-trail-sample.md)
|
|
6
21
|
|
|
7
22
|
## How It Works
|
|
8
23
|
|
|
@@ -16,6 +31,14 @@ Your AI Agent ←→ MCP (stdio) ←→ Persyst ←→ SQLite (local)
|
|
|
16
31
|
|
|
17
32
|
> 🚨 **First-Run Note**: On the first start, Persyst will automatically download the local embedding model (`all-MiniLM-L6-v2` ~50MB). This can take 30-60 seconds depending on your connection. The server will log `Loading embedding model...` and then proceed normally.
|
|
18
33
|
|
|
34
|
+
### Passive Recording vs. Active Retrieval
|
|
35
|
+
|
|
36
|
+
> ⚠️ **Honest Note on Agent Integration**: Persyst operates in two complementary modes:
|
|
37
|
+
> 1. **Passive Recording**: The file watcher automatically extracts and saves memories from your agent conversation transcripts in the background.
|
|
38
|
+
> 2. **Active Retrieval**: The AI agent calls `search_memories` or `get_optimized_context` to fetch relevant context.
|
|
39
|
+
>
|
|
40
|
+
> The IDE itself does not automatically inject retrieved memories into prompt inputs unless configured to do so via workspace rules (e.g. `.cursorrules`, `.windsurfrules`, `.agents/AGENTS.md`) or custom system prompt builders. To ensure the agent utilizes its memory, make sure your agent instructions direct it to query the database.
|
|
41
|
+
|
|
19
42
|
---
|
|
20
43
|
|
|
21
44
|
## Quick Start
|
|
@@ -116,6 +139,13 @@ Add Persyst to your Antigravity agent configuration file at `~/.gemini/antigravi
|
|
|
116
139
|
| `delete_memory` | Delete a memory and clean up edges | `id` (number) |
|
|
117
140
|
| `get_recent_memories` | Get latest memories | `limit` (number) |
|
|
118
141
|
| `get_important_memories` | Get by importance score | `limit` (number) |
|
|
142
|
+
| `get_optimized_context` | Get compressed, ranked context block | `query` (string), `max_tokens` (number) |
|
|
143
|
+
| `ingest_git_commits` | Import recent git commits as memories | `repo_path` (string), `count` (number) |
|
|
144
|
+
| `consolidate_memories` | Merge highly similar duplicate memories | — |
|
|
145
|
+
| `get_memory_history` | Retrieve all versions of a memory | `query` (string) |
|
|
146
|
+
| `get_agent_stats` | Agent reputation stats | — |
|
|
147
|
+
| `export_audit_log` | Export attestation audit log | `start_date`, `end_date` (ISO8601) |
|
|
148
|
+
| `verify_attestation` | Verify Ed25519 signature chain | `attestation_id` (string) |
|
|
119
149
|
|
|
120
150
|
---
|
|
121
151
|
|
|
@@ -159,6 +189,38 @@ Do not run `npx` with `sudo`. If you run into permission issues, ensure your npm
|
|
|
159
189
|
|
|
160
190
|
---
|
|
161
191
|
|
|
192
|
+
## Backup & Migration
|
|
193
|
+
|
|
194
|
+
Persyst includes built-in JSONL export/import commands for portable memory backup and cross-machine migration.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Export all memories to a file
|
|
198
|
+
npx persyst-mcp export
|
|
199
|
+
# → persyst-export-<timestamp>.jsonl
|
|
200
|
+
|
|
201
|
+
# Export to a specific file
|
|
202
|
+
npx persyst-mcp export my-backup.jsonl
|
|
203
|
+
|
|
204
|
+
# Preview what would be imported (dry run)
|
|
205
|
+
npx persyst-mcp import my-backup.jsonl --dry-run
|
|
206
|
+
|
|
207
|
+
# Import memories (skips exact & semantic duplicates automatically)
|
|
208
|
+
npx persyst-mcp import my-backup.jsonl
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Roadmap & Future Directions
|
|
214
|
+
|
|
215
|
+
Persyst is built for the privacy-focused solo developer. We are actively hardening the local-first experience before introducing network dependencies.
|
|
216
|
+
|
|
217
|
+
* **File-Based Sync** ✅ **Done**: `persyst-export` / `persyst-import` JSONL commands for backup and migration.
|
|
218
|
+
* **IDE Integrations**: First-class extensions for Cursor, VS Code, and Aider configuration helper commands.
|
|
219
|
+
* **True P2P Sync (Roadmap)**: Peer-to-peer secure sync between developer devices without relying on central cloud servers.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
162
223
|
## License
|
|
163
224
|
|
|
164
225
|
MIT License. See [LICENSE](LICENSE) for details.
|
|
226
|
+
|
package/bin/export.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* export.js — Persyst Memory JSONL Export CLI
|
|
5
|
+
*
|
|
6
|
+
* Exports all active memories to a portable JSONL file for backup or migration.
|
|
7
|
+
* Each line is a JSON object representing one memory with its full metadata.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* persyst-export → exports to persyst-export-<timestamp>.jsonl
|
|
11
|
+
* persyst-export memories.jsonl → exports to memories.jsonl
|
|
12
|
+
* persyst-export --namespace=shared → exports only the shared namespace
|
|
13
|
+
* persyst-export --all → includes archived (valid_until IS NOT NULL) memories
|
|
14
|
+
*
|
|
15
|
+
* The output format is designed to be imported back via `persyst-import`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createWriteStream } from 'fs';
|
|
19
|
+
import db, { closeDatabase } from '../src/database.js';
|
|
20
|
+
|
|
21
|
+
// ============================================================
|
|
22
|
+
// ARG PARSING
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const outputFile = args.find(a => !a.startsWith('--')) || `persyst-export-${Date.now()}.jsonl`;
|
|
27
|
+
const namespace = (args.find(a => a.startsWith('--namespace=')) || '').replace('--namespace=', '') || null;
|
|
28
|
+
const includeArchived = args.includes('--all');
|
|
29
|
+
|
|
30
|
+
// ============================================================
|
|
31
|
+
// EXPORT
|
|
32
|
+
// ============================================================
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const conditions = [];
|
|
36
|
+
const params = [];
|
|
37
|
+
|
|
38
|
+
if (!includeArchived) {
|
|
39
|
+
conditions.push('m.valid_until IS NULL');
|
|
40
|
+
}
|
|
41
|
+
if (namespace) {
|
|
42
|
+
conditions.push("(m.namespace = ? OR m.namespace = 'shared')");
|
|
43
|
+
params.push(namespace);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
47
|
+
|
|
48
|
+
const query = `
|
|
49
|
+
SELECT
|
|
50
|
+
m.id,
|
|
51
|
+
m.content,
|
|
52
|
+
m.importance_score,
|
|
53
|
+
m.namespace,
|
|
54
|
+
m.created_at,
|
|
55
|
+
m.last_accessed,
|
|
56
|
+
m.access_count,
|
|
57
|
+
m.parent_id,
|
|
58
|
+
m.valid_until,
|
|
59
|
+
p.source_type,
|
|
60
|
+
p.source_id,
|
|
61
|
+
p.confidence
|
|
62
|
+
FROM memories m
|
|
63
|
+
LEFT JOIN provenance p ON p.memory_id = m.id
|
|
64
|
+
${whereClause}
|
|
65
|
+
ORDER BY m.id ASC
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
const rows = params.length > 0 ? db.prepare(query).all(...params) : db.prepare(query).all();
|
|
69
|
+
|
|
70
|
+
const out = createWriteStream(outputFile, { encoding: 'utf8' });
|
|
71
|
+
|
|
72
|
+
let count = 0;
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
const record = {
|
|
75
|
+
id: row.id,
|
|
76
|
+
content: row.content,
|
|
77
|
+
importance_score: row.importance_score,
|
|
78
|
+
namespace: row.namespace || 'shared',
|
|
79
|
+
created_at: row.created_at,
|
|
80
|
+
last_accessed: row.last_accessed,
|
|
81
|
+
access_count: row.access_count,
|
|
82
|
+
parent_id: row.parent_id ?? null,
|
|
83
|
+
valid_until: row.valid_until ?? null,
|
|
84
|
+
provenance: row.source_type
|
|
85
|
+
? {
|
|
86
|
+
source_type: row.source_type,
|
|
87
|
+
source_id: row.source_id ?? null,
|
|
88
|
+
confidence: row.confidence ?? 1.0
|
|
89
|
+
}
|
|
90
|
+
: null
|
|
91
|
+
};
|
|
92
|
+
out.write(JSON.stringify(record) + '\n');
|
|
93
|
+
count++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await new Promise((resolve, reject) => {
|
|
97
|
+
out.end((err) => {
|
|
98
|
+
if (err) reject(err);
|
|
99
|
+
else resolve();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
console.log(`✅ Exported ${count} memories to: ${outputFile}`);
|
|
104
|
+
if (namespace) {
|
|
105
|
+
console.log(` Namespace filter: "${namespace}" + shared`);
|
|
106
|
+
}
|
|
107
|
+
if (includeArchived) {
|
|
108
|
+
console.log(' Includes archived (superseded) memories.');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(`❌ Export failed: ${err.message}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
} finally {
|
|
115
|
+
closeDatabase();
|
|
116
|
+
}
|
package/bin/import.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* import.js — Persyst Memory JSONL Import CLI
|
|
5
|
+
*
|
|
6
|
+
* Imports memories from a JSONL file created by `persyst-export`.
|
|
7
|
+
* Regenerates vector embeddings for each imported memory.
|
|
8
|
+
* Skips duplicates using both exact-content and semantic similarity checks.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* persyst-import memories.jsonl
|
|
12
|
+
* persyst-import memories.jsonl --dry-run → preview without writing
|
|
13
|
+
* persyst-import memories.jsonl --namespace=shared → force all into shared namespace
|
|
14
|
+
* persyst-import memories.jsonl --skip-embeddings → skip re-generating embeddings (fast, no semantic search for these)
|
|
15
|
+
*
|
|
16
|
+
* Compatible with the JSONL format produced by `persyst-export`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createReadStream } from 'fs';
|
|
20
|
+
import { createInterface } from 'readline';
|
|
21
|
+
import db, {
|
|
22
|
+
insertMemory,
|
|
23
|
+
insertVector,
|
|
24
|
+
memoryExists,
|
|
25
|
+
closeDatabase
|
|
26
|
+
} from '../src/database.js';
|
|
27
|
+
import { generateEmbedding } from '../src/embeddings.js';
|
|
28
|
+
import { searchHybrid } from '../src/search.js';
|
|
29
|
+
|
|
30
|
+
// ============================================================
|
|
31
|
+
// ARG PARSING
|
|
32
|
+
// ============================================================
|
|
33
|
+
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
const inputFile = args.find(a => !a.startsWith('--'));
|
|
36
|
+
const isDryRun = args.includes('--dry-run');
|
|
37
|
+
const forceNamespace = (args.find(a => a.startsWith('--namespace=')) || '').replace('--namespace=', '') || null;
|
|
38
|
+
const skipEmbeddings = args.includes('--skip-embeddings');
|
|
39
|
+
|
|
40
|
+
const DEDUP_THRESHOLD = 0.85;
|
|
41
|
+
|
|
42
|
+
if (!inputFile) {
|
|
43
|
+
console.error('❌ Usage: persyst-import <file.jsonl> [--dry-run] [--namespace=<ns>] [--skip-embeddings]');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================
|
|
48
|
+
// MAIN
|
|
49
|
+
// ============================================================
|
|
50
|
+
|
|
51
|
+
async function main() {
|
|
52
|
+
console.log(`📥 Persyst Import${isDryRun ? ' (DRY RUN — nothing will be written)' : ''}`);
|
|
53
|
+
console.log(` Source: ${inputFile}`);
|
|
54
|
+
if (forceNamespace) console.log(` Forcing namespace: "${forceNamespace}"`);
|
|
55
|
+
if (skipEmbeddings) console.log(' Skipping embedding regeneration.');
|
|
56
|
+
console.log('');
|
|
57
|
+
|
|
58
|
+
const rl = createInterface({
|
|
59
|
+
input: createReadStream(inputFile, { encoding: 'utf8' }),
|
|
60
|
+
crlfDelay: Infinity
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let lineNum = 0;
|
|
64
|
+
let imported = 0;
|
|
65
|
+
let skipped = 0;
|
|
66
|
+
let errors = 0;
|
|
67
|
+
|
|
68
|
+
for await (const line of rl) {
|
|
69
|
+
lineNum++;
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (!trimmed) continue;
|
|
72
|
+
|
|
73
|
+
let record;
|
|
74
|
+
try {
|
|
75
|
+
record = JSON.parse(trimmed);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(` ⚠️ Line ${lineNum}: Invalid JSON — skipping`);
|
|
78
|
+
errors++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { content, importance_score = 1.0, namespace, provenance, valid_until } = record;
|
|
83
|
+
|
|
84
|
+
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
|
85
|
+
console.error(` ⚠️ Line ${lineNum}: Empty content — skipping`);
|
|
86
|
+
errors++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Skip archived memories (unless they have a parent_id, we skip them anyway)
|
|
91
|
+
if (valid_until !== null && valid_until !== undefined) {
|
|
92
|
+
skipped++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const targetNamespace = forceNamespace || namespace || 'shared';
|
|
97
|
+
|
|
98
|
+
// --- Dedup: exact content match ---
|
|
99
|
+
if (memoryExists(content, targetNamespace)) {
|
|
100
|
+
console.log(` ⏭️ Line ${lineNum}: Already exists — skipping "${content.slice(0, 60)}..."`);
|
|
101
|
+
skipped++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Dedup: semantic similarity ---
|
|
106
|
+
if (!skipEmbeddings) {
|
|
107
|
+
try {
|
|
108
|
+
const similar = await searchHybrid(content, 1, null, null, targetNamespace);
|
|
109
|
+
if (similar.length > 0 && parseFloat(similar[0].similarity) >= DEDUP_THRESHOLD) {
|
|
110
|
+
console.log(` ⏭️ Line ${lineNum}: Semantically similar to #${similar[0].id} (sim=${similar[0].similarity}) — skipping`);
|
|
111
|
+
skipped++;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
} catch (_) {
|
|
115
|
+
// Non-critical: proceed with import if semantic check fails
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (isDryRun) {
|
|
120
|
+
console.log(` ✅ Would import: "${content.slice(0, 80)}${content.length > 80 ? '...' : ''}" → ns="${targetNamespace}"`);
|
|
121
|
+
imported++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Write to DB ---
|
|
126
|
+
try {
|
|
127
|
+
const prov = provenance || { source_type: 'import', source_id: 'persyst-import', confidence: 1.0 };
|
|
128
|
+
const id = insertMemory(content, importance_score, prov, targetNamespace);
|
|
129
|
+
|
|
130
|
+
if (!skipEmbeddings) {
|
|
131
|
+
const embedding = await generateEmbedding(content);
|
|
132
|
+
insertVector(id, embedding);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(` ✅ Imported #${id}: "${content.slice(0, 70)}${content.length > 70 ? '...' : ''}"`);
|
|
136
|
+
imported++;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(` ❌ Line ${lineNum}: Failed to insert — ${err.message}`);
|
|
139
|
+
errors++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log('═'.repeat(50));
|
|
145
|
+
if (isDryRun) {
|
|
146
|
+
console.log(`📊 Dry run complete: ${imported} would import, ${skipped} skipped, ${errors} errors`);
|
|
147
|
+
} else {
|
|
148
|
+
console.log(`📊 Import complete: ${imported} imported, ${skipped} skipped, ${errors} errors`);
|
|
149
|
+
}
|
|
150
|
+
console.log('═'.repeat(50));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main()
|
|
154
|
+
.catch(err => {
|
|
155
|
+
console.error(`❌ Import crashed: ${err.message}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
})
|
|
158
|
+
.finally(() => {
|
|
159
|
+
closeDatabase();
|
|
160
|
+
});
|
package/bin/init.js
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* persyst-init — Workspace rules generator
|
|
4
|
+
* persyst-init — Workspace rules generator and global IDE configuration builder
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* npx persyst-mcp init
|
|
8
|
+
* npx persyst-mcp init --mcp cursor,aider
|
|
8
9
|
*
|
|
9
10
|
* What it does:
|
|
10
|
-
* 1. Safely creates or appends system instructions to `.cursorrules`
|
|
11
|
-
* 2.
|
|
12
|
-
* 3.
|
|
13
|
-
* 4.
|
|
14
|
-
*
|
|
15
|
-
* Design:
|
|
16
|
-
* - Non-destructive: checks for existing content before appending to avoid duplication
|
|
17
|
-
* - Idempotent: safe to run multiple times
|
|
18
|
-
* - Localized: targets the current working directory (project root)
|
|
11
|
+
* 1. Safely creates or appends system instructions to `.cursorrules` and `.windsurfrules`
|
|
12
|
+
* 2. Creates a general `.persystrules.md` workspace guide
|
|
13
|
+
* 3. Configures Git post-commit hook for auto-ingestion
|
|
14
|
+
* 4. Generates cryptographic Ed25519 keys inside ~/.persyst
|
|
15
|
+
* 5. Automatically detects and configures global settings for Cursor, Aider, Claude Code, and Continue
|
|
19
16
|
*/
|
|
20
17
|
|
|
21
18
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
|
|
22
19
|
import { join, resolve, dirname } from 'path';
|
|
20
|
+
import { homedir } from 'os';
|
|
23
21
|
import { fileURLToPath } from 'url';
|
|
22
|
+
import { execSync } from 'child_process';
|
|
23
|
+
import { initializeKeys } from '../src/attestation.js';
|
|
24
24
|
|
|
25
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
26
|
const __dirname = dirname(__filename);
|
|
27
27
|
|
|
28
|
+
const CONFIG_DIR = join(homedir(), '.persyst');
|
|
29
|
+
const PERSYST_DB = join(CONFIG_DIR, 'persyst.db');
|
|
30
|
+
|
|
28
31
|
// ============================================================
|
|
29
32
|
// SYSTEM INSTRUCTION CONTENT
|
|
30
33
|
// ============================================================
|
|
@@ -46,6 +49,10 @@ You are integrated with Persyst, a local-first MCP memory server that stores use
|
|
|
46
49
|
- Handle Contradictions: Persyst handles contradiction detection automatically. If a new fact contradicts an old memory, Persyst will flag it.
|
|
47
50
|
- Quality Over Quantity: Do NOT store trivial facts, temporary conversation noise, or duplicate data. "Bad data is worse than no data". Only store long-term architecture decisions, project details, and explicit user preferences.
|
|
48
51
|
|
|
52
|
+
## Explicit User Save Requests
|
|
53
|
+
- If the user explicitly asks you to remember, save, or keep a note of a fact (e.g., "Remember that John handles deployment", "remind me that staging is flaky"), call the \`add_memory\` tool immediately with that content.
|
|
54
|
+
- Bypassing Tech Filters: Explicit user requests bypass the programming keyword filters. Ensure they are captured verbatim.
|
|
55
|
+
|
|
49
56
|
## Mandatory Completion Checklist (HARD CONSTRAINT)
|
|
50
57
|
Before writing your final response declaring a task, feature, or bug fix complete:
|
|
51
58
|
1. Ask yourself: "Did I implement a feature, fix a bug, configure a tool, or discover a project rule?"
|
|
@@ -82,7 +89,7 @@ ${RULE_CONTENT.trim()}
|
|
|
82
89
|
`;
|
|
83
90
|
|
|
84
91
|
// ============================================================
|
|
85
|
-
// HELPERS
|
|
92
|
+
// WORKSPACE HELPERS
|
|
86
93
|
// ============================================================
|
|
87
94
|
|
|
88
95
|
function setupRuleFile(filePath, fileName) {
|
|
@@ -104,33 +111,153 @@ function setupRuleFile(filePath, fileName) {
|
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
// ============================================================
|
|
107
|
-
//
|
|
114
|
+
// GLOBAL CONFIG WRITERS
|
|
115
|
+
// ============================================================
|
|
116
|
+
|
|
117
|
+
function detectEditors() {
|
|
118
|
+
const editors = [];
|
|
119
|
+
const home = homedir();
|
|
120
|
+
|
|
121
|
+
// Cursor
|
|
122
|
+
const cursorDir = join(home, '.cursor');
|
|
123
|
+
const winCursorDir = join(home, 'AppData', 'Roaming', 'Cursor');
|
|
124
|
+
if (existsSync(cursorDir) || existsSync(winCursorDir) || existsSync('/Applications/Cursor.app') || existsSync(join(home, 'AppData', 'Local', 'Programs', 'cursor'))) {
|
|
125
|
+
editors.push('cursor');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Aider
|
|
129
|
+
try {
|
|
130
|
+
execSync('aider --version', { stdio: 'ignore' });
|
|
131
|
+
editors.push('aider');
|
|
132
|
+
} catch (_) {}
|
|
133
|
+
|
|
134
|
+
// Claude Code
|
|
135
|
+
const claudeDir = join(home, '.claude');
|
|
136
|
+
if (existsSync(claudeDir) || existsSync('/Applications/Claude Code.app')) {
|
|
137
|
+
editors.push('claude-code');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Continue.dev
|
|
141
|
+
const continueConfig = join(home, '.continue', 'config.json');
|
|
142
|
+
if (existsSync(continueConfig)) {
|
|
143
|
+
editors.push('continue');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return editors;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function writeCursorConfig() {
|
|
150
|
+
const cursorMcp = join(homedir(), '.cursor', 'mcp.json');
|
|
151
|
+
try {
|
|
152
|
+
const config = existsSync(cursorMcp) ? JSON.parse(readFileSync(cursorMcp, 'utf8')) : {};
|
|
153
|
+
config.mcpServers = config.mcpServers || {};
|
|
154
|
+
config.mcpServers.persyst = {
|
|
155
|
+
"command": "npx",
|
|
156
|
+
"args": ["-y", "persyst-mcp"],
|
|
157
|
+
"env": { "PERSYST_DB": PERSYST_DB }
|
|
158
|
+
};
|
|
159
|
+
mkdirSync(dirname(cursorMcp), { recursive: true });
|
|
160
|
+
writeFileSync(cursorMcp, JSON.stringify(config, null, 2));
|
|
161
|
+
console.log(' ✅ Cursor MCP config written to ~/.cursor/mcp.json');
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(` ✗ Failed to configure Cursor: ${err.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function writeAiderConfig() {
|
|
168
|
+
const aiderYml = join(homedir(), '.aider.conf.yml');
|
|
169
|
+
try {
|
|
170
|
+
let content = '';
|
|
171
|
+
if (existsSync(aiderYml)) {
|
|
172
|
+
content = readFileSync(aiderYml, 'utf8');
|
|
173
|
+
}
|
|
174
|
+
if (!content.includes('persyst')) {
|
|
175
|
+
content += `\n# Persyst MCP integration\nmcp:\n - name: persyst\n cmd: npx\n args: ["-y", "persyst-mcp"]\n env:\n PERSYST_DB: ${PERSYST_DB}\n`;
|
|
176
|
+
writeFileSync(aiderYml, content);
|
|
177
|
+
console.log(' ✅ Aider MCP config appended to ~/.aider.conf.yml');
|
|
178
|
+
} else {
|
|
179
|
+
console.log(' ℹ️ Aider already has Persyst configured (skipped).');
|
|
180
|
+
}
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error(` ✗ Failed to configure Aider: ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function writeClaudeCodeConfig() {
|
|
187
|
+
const claudeJson = join(homedir(), '.claude.json');
|
|
188
|
+
try {
|
|
189
|
+
const config = existsSync(claudeJson) ? JSON.parse(readFileSync(claudeJson, 'utf8')) : {};
|
|
190
|
+
config.mcpServers = config.mcpServers || {};
|
|
191
|
+
config.mcpServers.persyst = {
|
|
192
|
+
"command": "npx",
|
|
193
|
+
"args": ["-y", "persyst-mcp"],
|
|
194
|
+
"env": { "PERSYST_DB": PERSYST_DB }
|
|
195
|
+
};
|
|
196
|
+
writeFileSync(claudeJson, JSON.stringify(config, null, 2));
|
|
197
|
+
console.log(' ✅ Claude Code MCP config written to ~/.claude.json');
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error(` ✗ Failed to configure Claude Code: ${err.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function writeContinueConfig() {
|
|
204
|
+
const continueConfig = join(homedir(), '.continue', 'config.json');
|
|
205
|
+
try {
|
|
206
|
+
const config = existsSync(continueConfig) ? JSON.parse(readFileSync(continueConfig, 'utf8')) : {};
|
|
207
|
+
config.mcpServers = config.mcpServers || [];
|
|
208
|
+
// Remove existing persyst entry
|
|
209
|
+
config.mcpServers = config.mcpServers.filter(s => s.name !== 'persyst');
|
|
210
|
+
config.mcpServers.push({
|
|
211
|
+
"name": "persyst",
|
|
212
|
+
"command": "npx",
|
|
213
|
+
"args": ["-y", "persyst-mcp"],
|
|
214
|
+
"env": { "PERSYST_DB": PERSYST_DB }
|
|
215
|
+
});
|
|
216
|
+
mkdirSync(dirname(continueConfig), { recursive: true });
|
|
217
|
+
writeFileSync(continueConfig, JSON.stringify(config, null, 2));
|
|
218
|
+
console.log(' ✅ Continue.dev MCP config written to ~/.continue/config.json');
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error(` ✗ Failed to configure Continue.dev: ${err.message}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ============================================================
|
|
225
|
+
// MAIN RUNNER
|
|
108
226
|
// ============================================================
|
|
109
227
|
|
|
110
228
|
function run() {
|
|
111
229
|
console.log('');
|
|
112
|
-
console.log(' 🧠 Persyst — Workspace
|
|
113
|
-
console.log('
|
|
230
|
+
console.log(' 🧠 Persyst — Workspace & Editor Setup');
|
|
231
|
+
console.log(' ══════════════════════════════════════');
|
|
114
232
|
console.log('');
|
|
115
233
|
|
|
116
234
|
const cwd = process.cwd();
|
|
117
235
|
console.log(` 📁 Target workspace: ${cwd}`);
|
|
118
|
-
console.log('');
|
|
119
236
|
|
|
120
|
-
// 1.
|
|
237
|
+
// 1. Initialize local configuration folder and attestations
|
|
238
|
+
console.log(' ⚙️ Initializing keypairs & DB folders...');
|
|
239
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
240
|
+
initializeKeys();
|
|
241
|
+
console.log(' ✅ Cryptographic keypairs generated');
|
|
242
|
+
|
|
243
|
+
// 2. Local workspace configurations
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log(' 📄 Initializing workspace rule files...');
|
|
246
|
+
|
|
121
247
|
const cursorRulesPath = join(cwd, '.cursorrules');
|
|
122
248
|
setupRuleFile(cursorRulesPath, '.cursorrules');
|
|
123
249
|
|
|
124
|
-
// 2. Create/Append Windsurf Rules
|
|
125
250
|
const windsurfRulesPath = join(cwd, '.windsurfrules');
|
|
126
251
|
setupRuleFile(windsurfRulesPath, '.windsurfrules');
|
|
127
252
|
|
|
128
|
-
|
|
253
|
+
const clineRulesPath = join(cwd, '.clinerules');
|
|
254
|
+
setupRuleFile(clineRulesPath, '.clinerules');
|
|
255
|
+
|
|
129
256
|
const generalGuidePath = join(cwd, '.persystrules.md');
|
|
130
257
|
writeFileSync(generalGuidePath, GENERAL_GUIDE.trim() + '\n', 'utf8');
|
|
131
258
|
console.log(' ✅ Created .persystrules.md (General Guide)');
|
|
132
259
|
|
|
133
|
-
//
|
|
260
|
+
// 3. Git post-commit hook
|
|
134
261
|
const gitDir = join(cwd, '.git');
|
|
135
262
|
if (existsSync(gitDir)) {
|
|
136
263
|
const hooksDir = join(gitDir, 'hooks');
|
|
@@ -159,22 +286,31 @@ fi
|
|
|
159
286
|
console.log(' ✅ Configured Git post-commit hook for auto-ingestion');
|
|
160
287
|
}
|
|
161
288
|
|
|
162
|
-
//
|
|
289
|
+
// 4. Global editor configurations
|
|
163
290
|
console.log('');
|
|
164
|
-
console.log('
|
|
165
|
-
|
|
291
|
+
console.log(' 💻 Initializing global IDE configurations...');
|
|
292
|
+
|
|
293
|
+
const args = process.argv.slice(2);
|
|
294
|
+
const mcpFlag = args.find(a => a.startsWith('--mcp='));
|
|
295
|
+
const requestedEditors = mcpFlag ? mcpFlag.split('=')[1].split(',') : [];
|
|
296
|
+
|
|
297
|
+
const editors = requestedEditors.length > 0 ? requestedEditors : detectEditors();
|
|
298
|
+
console.log(` Detected editors/environments: ${editors.join(', ') || 'none'}`);
|
|
299
|
+
|
|
300
|
+
if (editors.includes('cursor')) writeCursorConfig();
|
|
301
|
+
if (editors.includes('aider')) writeAiderConfig();
|
|
302
|
+
if (editors.includes('claude-code')) writeClaudeCodeConfig();
|
|
303
|
+
if (editors.includes('continue')) writeContinueConfig();
|
|
304
|
+
|
|
305
|
+
// 5. Final self-test and notes
|
|
166
306
|
console.log('');
|
|
167
|
-
console.log('
|
|
168
|
-
console.log('
|
|
169
|
-
console.log(' 2. Add a new command server:');
|
|
170
|
-
console.log(' • Name: persyst');
|
|
171
|
-
console.log(' • Command: npx');
|
|
172
|
-
console.log(' • Arguments: -y persyst-mcp');
|
|
307
|
+
console.log(' ══════════════════════════════════════');
|
|
308
|
+
console.log(' 🎉 Setup complete! Persyst is fully configured.');
|
|
173
309
|
console.log('');
|
|
174
|
-
console.log('
|
|
175
|
-
console.log('
|
|
176
|
-
console.log('
|
|
177
|
-
console.log('
|
|
310
|
+
console.log(' Next steps:');
|
|
311
|
+
console.log(' 1. Restart your editor to load the new MCP configurations.');
|
|
312
|
+
console.log(' 2. Test gateway connection:');
|
|
313
|
+
console.log(' curl http://127.0.0.1:4321/health');
|
|
178
314
|
console.log('');
|
|
179
315
|
}
|
|
180
316
|
|