prism-mcp-server 12.5.6 โ 13.0.0
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 +46 -6
- package/dist/aba-protocol.js +3 -2
- package/dist/config.js +30 -15
- package/dist/darkfactory/cloudDelegate.js +7 -0
- package/dist/dashboard/server.js +7 -9
- package/dist/lifecycle.js +32 -4
- package/dist/scholar/freeSearch.js +30 -2
- package/dist/scholar/webScholar.js +10 -25
- package/dist/scm/client.js +13 -5
- package/dist/scm/githubSync.js +1 -1
- package/dist/storage/index.js +39 -18
- package/dist/storage/synalux.js +272 -0
- package/dist/sync/encryptedSync.js +17 -4
- package/dist/sync/sqliteSync.js +4 -2
- package/dist/sync/supabaseSync.js +2 -0
- package/dist/tools/adaptiveDefinitions.js +148 -0
- package/dist/tools/definitions.js +1 -1
- package/dist/tools/graphHandlers.js +4 -0
- package/dist/tools/ledgerHandlers.js +91 -24
- package/dist/tools/sessionMemoryDefinitions.js +2 -1
- package/dist/tools/skillRouting.js +86 -0
- package/dist/tools/v12Handlers.js +11 -1
- package/dist/utils/analytics.js +3 -1
- package/dist/utils/braveApi.js +5 -0
- package/dist/utils/factMerger.js +5 -4
- package/dist/utils/googleSearchApi.js +3 -1
- package/dist/utils/healthCheck.js +3 -3
- package/dist/utils/notifier.js +56 -0
- package/dist/utils/projectResolver.js +112 -0
- package/dist/utils/sanitizer.js +5 -1
- package/dist/utils/supabaseApi.js +1 -0
- package/package.json +2 -3
- package/dist/utils/tavilyApi.js +0 -70
package/README.md
CHANGED
|
@@ -146,7 +146,7 @@ Prism v11.6.0 introduces **production-grade agent infrastructure** for running m
|
|
|
146
146
|
|
|
147
147
|
## ๐ฌ <a name="deep-research-intelligence"></a>v11.5.1 Deep Research Intelligence (Auto-Scholar)
|
|
148
148
|
|
|
149
|
-
Prism v11.5.1 transforms your AI agent from a "Coder" into a "Clinical Scientist." It features a **
|
|
149
|
+
Prism v11.5.1 transforms your AI agent from a "Coder" into a "Clinical Scientist." It features a **Multi-Provider Discovery Pipeline** (Brave + Firecrawl + Google Scholar) that grounds Gemini 2.5 Flash's thinking in real-world empirical data.
|
|
150
150
|
|
|
151
151
|
### ๐ฅ The Global Benchmarks: Prism v11 vs. Standard RAG
|
|
152
152
|
|
|
@@ -176,7 +176,7 @@ Prism features a cutting-edge **Zero-Search Retrieval** system for its cognitive
|
|
|
176
176
|
|
|
177
177
|
### ๐ Supported Discovery Engines & Databases
|
|
178
178
|
|
|
179
|
-
1. **
|
|
179
|
+
1. **Brave Search + Firecrawl** (Primary): Web search + deep scraping for full-text article extraction.
|
|
180
180
|
2. **PubMed (NCBI)** (Clinical): The world's largest biomedical database for clinical citations.
|
|
181
181
|
3. **ERIC (Education Research)** (Behavioral): The definitive database for ABA and pediatric interventions.
|
|
182
182
|
4. **Semantic Scholar** (Academic): AI-powered research tool providing "TLDR" summaries of 200M+ papers.
|
|
@@ -195,6 +195,7 @@ Prism features a cutting-edge **Zero-Search Retrieval** system for its cognitive
|
|
|
195
195
|
| **BFCL Submission** | [PR #1332](https://github.com/ShishirPatil/gorilla/pull/1332) |
|
|
196
196
|
| **Changelog** | [synalux-docs/CHANGELOG.md](https://github.com/dcostenco/synalux-docs/blob/main/CHANGELOG.md) |
|
|
197
197
|
| **Roadmap** | [synalux-docs/ROADMAP.md](https://github.com/dcostenco/synalux-docs/blob/main/ROADMAP.md) |
|
|
198
|
+
| **PrismAAC** | [github.com/dcostenco/prism-aac](https://github.com/dcostenco/prism-aac) โ AAC web app for children with motor impairments |
|
|
198
199
|
|
|
199
200
|
---
|
|
200
201
|
|
|
@@ -296,7 +297,7 @@ Then open `http://localhost:3001` instead.
|
|
|
296
297
|
| **Ledger compaction** | โ
`prism-coder:7b` via Ollama | โ
Text provider key |
|
|
297
298
|
| **Task routing (LLM tiebreaker)** | โ
`prism-coder:7b` via Ollama | N/A (heuristic-only) |
|
|
298
299
|
| Morning Briefings | โ | โ
Text provider key |
|
|
299
|
-
| Web Scholar research | โ | โ
[`BRAVE_API_KEY`](#environment-variables) + [`FIRECRAWL_API_KEY`](#environment-variables)
|
|
300
|
+
| Web Scholar research | โ | โ
[`BRAVE_API_KEY`](#environment-variables) + [`FIRECRAWL_API_KEY`](#environment-variables) |
|
|
300
301
|
| VLM image captioning | โ | โ
Provider key |
|
|
301
302
|
| Autonomous Pipelines (Dark Factory) | โ | โ
Text provider key |
|
|
302
303
|
|
|
@@ -327,6 +328,9 @@ Then open `http://localhost:3001` instead.
|
|
|
327
328
|
|
|
328
329
|
---
|
|
329
330
|
|
|
331
|
+
<details>
|
|
332
|
+
<summary><strong>๐ Setup Guides</strong> โ MCP client configs, schema migrations, troubleshooting <em>(technical ยท click to expand)</em></summary>
|
|
333
|
+
|
|
330
334
|
## <a name="setup-guides"></a>๐ Setup Guides
|
|
331
335
|
|
|
332
336
|
<details>
|
|
@@ -608,6 +612,8 @@ Prism can be deployed natively to cloud platforms like [Render](https://render.c
|
|
|
608
612
|
> ```
|
|
609
613
|
> Claude Code users can use the `.clauderules` auto-load hook shown in the [Setup Guides](#setup-guides). Prism also has a **server-side fallback** (v5.2.1+) that auto-pushes context after 10 seconds if no load is detected.
|
|
610
614
|
|
|
615
|
+
</details>
|
|
616
|
+
|
|
611
617
|
---
|
|
612
618
|
|
|
613
619
|
## <a name="universal-import-bring-your-history"></a>๐ฅ Universal Import: Bring Your History
|
|
@@ -1347,6 +1353,9 @@ prism scm dora --repo synalux/portal --period 2024-Q4
|
|
|
1347
1353
|
|
|
1348
1354
|
---
|
|
1349
1355
|
|
|
1356
|
+
<details>
|
|
1357
|
+
<summary><strong>๐ป CLI Reference</strong> โ commands for CI/CD, scripts, and non-MCP environments <em>(technical ยท click to expand)</em></summary>
|
|
1358
|
+
|
|
1350
1359
|
## <a name="cli-reference"></a>๐ป CLI Reference
|
|
1351
1360
|
|
|
1352
1361
|
Prism includes a CLI for environments where MCP tools aren't available (CI/CD pipelines, Bash scripts, non-MCP IDEs like Antigravity).
|
|
@@ -1378,6 +1387,13 @@ prism verify generate # Bless current rubric as canonic
|
|
|
1378
1387
|
|
|
1379
1388
|
---
|
|
1380
1389
|
|
|
1390
|
+
</details>
|
|
1391
|
+
|
|
1392
|
+
---
|
|
1393
|
+
|
|
1394
|
+
<details>
|
|
1395
|
+
<summary><strong>๐ง Tool Reference</strong> โ full schema for all 30+ MCP tools <em>(technical ยท click to expand)</em></summary>
|
|
1396
|
+
|
|
1381
1397
|
## <a name="tool-reference"></a>๐ง Tool Reference
|
|
1382
1398
|
|
|
1383
1399
|
Prism ships 30+ tools, but **90% of your workflow uses just three:**
|
|
@@ -1511,6 +1527,13 @@ Requires `PRISM_DARK_FACTORY_ENABLED=true`.
|
|
|
1511
1527
|
|
|
1512
1528
|
---
|
|
1513
1529
|
|
|
1530
|
+
</details>
|
|
1531
|
+
|
|
1532
|
+
---
|
|
1533
|
+
|
|
1534
|
+
<details>
|
|
1535
|
+
<summary><strong>โ๏ธ Environment Variables</strong> โ full config reference, env vars, dashboard settings <em>(technical ยท click to expand)</em></summary>
|
|
1536
|
+
|
|
1514
1537
|
## <a name="environment-variables"></a>Environment Variables
|
|
1515
1538
|
|
|
1516
1539
|
> **๐ฆ TL;DR โ Just want the best experience fast?** Two options:
|
|
@@ -1521,7 +1544,7 @@ Requires `PRISM_DARK_FACTORY_ENABLED=true`.
|
|
|
1521
1544
|
> # Option B: Cloud-powered (best quality)
|
|
1522
1545
|
> GOOGLE_API_KEY=... # Unlocks: Gemini embeddings, Morning Briefings, auto-compaction
|
|
1523
1546
|
> BRAVE_API_KEY=... # Unlocks: Web Scholar research + Brave Answers
|
|
1524
|
-
> FIRECRAWL_API_KEY=... # Unlocks: Web Scholar deep scraping
|
|
1547
|
+
> FIRECRAWL_API_KEY=... # Unlocks: Web Scholar deep scraping
|
|
1525
1548
|
> ```
|
|
1526
1549
|
> **Zero keys = zero problem.** Core session memory, keyword search, semantic search (local embeddings), time travel, and the full dashboard work 100% offline. Cloud keys are optional power-ups.
|
|
1527
1550
|
|
|
@@ -1531,8 +1554,7 @@ Requires `PRISM_DARK_FACTORY_ENABLED=true`.
|
|
|
1531
1554
|
| Variable | Required | Description |
|
|
1532
1555
|
|----------|----------|-------------|
|
|
1533
1556
|
| `BRAVE_API_KEY` | No | Brave Search Pro API key |
|
|
1534
|
-
| `FIRECRAWL_API_KEY` | No | Firecrawl API key โ required for Web Scholar
|
|
1535
|
-
| `TAVILY_API_KEY` | No | Tavily Search API key โ alternative to Brave+Firecrawl for Web Scholar |
|
|
1557
|
+
| `FIRECRAWL_API_KEY` | No | Firecrawl API key โ required for Web Scholar deep scraping |
|
|
1536
1558
|
| `PRISM_STORAGE` | No | `"local"` (default) or `"supabase"` โ restart required |
|
|
1537
1559
|
| `PRISM_ENABLE_HIVEMIND` | No | `"true"` to enable multi-agent tools โ restart required |
|
|
1538
1560
|
| `PRISM_INSTANCE` | No | Instance name for multi-server PID isolation |
|
|
@@ -1576,6 +1598,13 @@ Some configurations are stored dynamically in SQLite (`system_settings` table) a
|
|
|
1576
1598
|
|
|
1577
1599
|
---
|
|
1578
1600
|
|
|
1601
|
+
</details>
|
|
1602
|
+
|
|
1603
|
+
---
|
|
1604
|
+
|
|
1605
|
+
<details>
|
|
1606
|
+
<summary><strong>๐๏ธ Architecture</strong> โ startup sequence, storage layers, auto-load mechanics <em>(technical ยท click to expand)</em></summary>
|
|
1607
|
+
|
|
1579
1608
|
## <a name="architecture"></a>Architecture
|
|
1580
1609
|
|
|
1581
1610
|
Prism is a **stdio-based MCP server** that manages persistent agent memory. Here's how the pieces fit together:
|
|
@@ -1652,6 +1681,13 @@ All platforms benefit from the **server-side fallback** (v5.2.1): if `session_lo
|
|
|
1652
1681
|
|
|
1653
1682
|
---
|
|
1654
1683
|
|
|
1684
|
+
</details>
|
|
1685
|
+
|
|
1686
|
+
---
|
|
1687
|
+
|
|
1688
|
+
<details>
|
|
1689
|
+
<summary><strong>๐งฌ Scientific Foundation</strong> โ cognitive science citations, ACT-R, HRR, peer-reviewed models <em>(technical ยท click to expand)</em></summary>
|
|
1690
|
+
|
|
1655
1691
|
## <a name="scientific-foundation"></a>๐งฌ Scientific Foundation
|
|
1656
1692
|
|
|
1657
1693
|
Prism has evolved from smart session logging into a **cognitive memory architecture** โ grounded in real research, not marketing. Every retrieval decision is backed by peer-reviewed models from cognitive psychology, neuroscience, and distributed computing.
|
|
@@ -1712,6 +1748,10 @@ The core unbinding engine is verified via Synalux's cognitive testing suite:
|
|
|
1712
1748
|
|
|
1713
1749
|
---
|
|
1714
1750
|
|
|
1751
|
+
</details>
|
|
1752
|
+
|
|
1753
|
+
---
|
|
1754
|
+
|
|
1715
1755
|
## ๐ผ B2B Consulting & Enterprise Support
|
|
1716
1756
|
|
|
1717
1757
|
Prism MCP is open-source and free for individual developers. For teams and enterprises building autonomous AI workflows or integrating MCP-native memory at scale, we offer professional consulting and setup packages.
|
package/dist/aba-protocol.js
CHANGED
|
@@ -100,9 +100,10 @@ export function buildVSCodePrompt(identity) {
|
|
|
100
100
|
].join('\n');
|
|
101
101
|
}
|
|
102
102
|
// โโโ Input Sanitization โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
103
|
-
|
|
103
|
+
import { sanitizeMcpOutput } from './utils/sanitizer.js';
|
|
104
|
+
/** Strip XML-like tags that could hijack system instructions โ delegates to shared sanitizer */
|
|
104
105
|
export function sanitizeUserInput(text) {
|
|
105
|
-
return text
|
|
106
|
+
return sanitizeMcpOutput(text);
|
|
106
107
|
}
|
|
107
108
|
/** Wrap user input in <user_input> tags after sanitization */
|
|
108
109
|
export function wrapUserInput(text) {
|
package/dist/config.js
CHANGED
|
@@ -78,17 +78,25 @@ export const VOYAGE_API_KEY = process.env.VOYAGE_API_KEY;
|
|
|
78
78
|
// Get yours at: https://developers.google.com/custom-search/v1/overview
|
|
79
79
|
export const GOOGLE_SEARCH_API_KEY = process.env.GOOGLE_SEARCH_API_KEY;
|
|
80
80
|
export const GOOGLE_SEARCH_CX = process.env.GOOGLE_SEARCH_CX;
|
|
81
|
-
// โโโ v2.0 / v12.1: Storage Backend Selection
|
|
82
|
-
//
|
|
81
|
+
// โโโ v2.0 / v12.1 / v13: Storage Backend Selection โโโโโโโโโโ
|
|
82
|
+
// Three backends are implemented:
|
|
83
|
+
// "local" โ SQLite, fully offline. Free-tier default.
|
|
84
|
+
// "supabase" โ direct Supabase REST. Legacy direct-write path. Deprecated for paid tiers.
|
|
85
|
+
// "synalux" โ thin HTTP client of synalux portal. Paid-tier default. Mediates project
|
|
86
|
+
// validation, tier gating, and audit. See SynaluxStorage.
|
|
83
87
|
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// Set PRISM_STORAGE=local to force local SQLite.
|
|
90
|
-
// Set PRISM_STORAGE=supabase to force Supabase REST API.
|
|
88
|
+
// Auto-resolution (PRISM_STORAGE=auto, the default) picks in this order:
|
|
89
|
+
// 1. PRISM_FORCE_LOCAL=true โ "local" (override everything)
|
|
90
|
+
// 2. SYNALUX_API_KEY + PRISM_SYNALUX_BASE_URL set โ "synalux"
|
|
91
|
+
// 3. SUPABASE_URL + SUPABASE_KEY set โ "supabase" (legacy)
|
|
92
|
+
// 4. else โ "local"
|
|
91
93
|
export const PRISM_STORAGE = process.env.PRISM_STORAGE || "auto";
|
|
94
|
+
/**
|
|
95
|
+
* Hard override โ when true, forces local SQLite regardless of any cloud
|
|
96
|
+
* credentials. Used by free-tier installs and HIPAA deployments that must
|
|
97
|
+
* never touch the network for memory operations.
|
|
98
|
+
*/
|
|
99
|
+
export const PRISM_FORCE_LOCAL = process.env.PRISM_FORCE_LOCAL === "true";
|
|
92
100
|
// Logged at debug level โ see debug() at bottom of file
|
|
93
101
|
// โโโ Optional: Supabase (Session Memory Module) โโโโโโโโโโโโโโโ
|
|
94
102
|
// When both SUPABASE_URL and SUPABASE_KEY are set, session memory tools
|
|
@@ -117,6 +125,16 @@ export const SUPABASE_KEY = sanitizeEnv(process.env.SUPABASE_KEY);
|
|
|
117
125
|
export const SUPABASE_CONFIGURED = !!SUPABASE_URL &&
|
|
118
126
|
!!SUPABASE_KEY &&
|
|
119
127
|
isHttpUrl(SUPABASE_URL);
|
|
128
|
+
// โโโ Synalux Cloud Backend (thin-client mode) โโโโโโโโโโโโโโโ
|
|
129
|
+
// When PRISM_SYNALUX_BASE_URL + PRISM_SYNALUX_API_KEY are set, the MCP
|
|
130
|
+
// becomes a thin HTTP client of the synalux portal. This is the paid-tier
|
|
131
|
+
// default. Synalux portal owns project validation, tier gating, audit logs,
|
|
132
|
+
// and hivemind agent coordination.
|
|
133
|
+
export const PRISM_SYNALUX_BASE_URL = sanitizeEnv(process.env.PRISM_SYNALUX_BASE_URL);
|
|
134
|
+
export const PRISM_SYNALUX_API_KEY = sanitizeEnv(process.env.PRISM_SYNALUX_API_KEY);
|
|
135
|
+
export const SYNALUX_CONFIGURED = !!PRISM_SYNALUX_BASE_URL &&
|
|
136
|
+
!!PRISM_SYNALUX_API_KEY &&
|
|
137
|
+
isHttpUrl(PRISM_SYNALUX_BASE_URL);
|
|
120
138
|
if (process.env.SUPABASE_URL && !SUPABASE_URL) {
|
|
121
139
|
console.error("Warning: SUPABASE_URL appears unresolved/empty (e.g. template placeholder). Falling back to local storage unless explicitly fixed.");
|
|
122
140
|
}
|
|
@@ -179,13 +197,10 @@ export const PRISM_SCHEDULER_INTERVAL_MS = parseInt(process.env.PRISM_SCHEDULER_
|
|
|
179
197
|
);
|
|
180
198
|
// โโโ v5.4: Autonomous Web Scholar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
181
199
|
// Background LLM research pipeline powered by Brave Search + Firecrawl.
|
|
182
|
-
// Tavily can be used as an alternative when TAVILY_API_KEY is set.
|
|
183
|
-
// Defaults are conservative to prevent runaway API costs.
|
|
184
200
|
export const FIRECRAWL_API_KEY = process.env.FIRECRAWL_API_KEY;
|
|
185
|
-
export const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
console.error("Warning: Neither FIRECRAWL_API_KEY nor TAVILY_API_KEY is set. Web Scholar will fall back to free search.");
|
|
201
|
+
export const PRISM_SCHOLAR_ENABLED = process.env.PRISM_SCHOLAR_ENABLED === "true";
|
|
202
|
+
if (PRISM_SCHOLAR_ENABLED && !FIRECRAWL_API_KEY) {
|
|
203
|
+
console.error("Warning: FIRECRAWL_API_KEY not set. Web Scholar will fall back to free search.");
|
|
189
204
|
}
|
|
190
205
|
export const PRISM_SCHOLAR_INTERVAL_MS = parseInt(process.env.PRISM_SCHOLAR_INTERVAL_MS || "0", 10 // Default manual-only
|
|
191
206
|
);
|
|
@@ -17,6 +17,9 @@ let config = {
|
|
|
17
17
|
const activeTasks = new Map();
|
|
18
18
|
const taskHistory = [];
|
|
19
19
|
export function configureDelegate(updates) {
|
|
20
|
+
if (updates.endpoint && !updates.endpoint.startsWith('https://') && !updates.endpoint.includes('localhost')) {
|
|
21
|
+
throw new Error('Cloud delegate endpoint must use HTTPS');
|
|
22
|
+
}
|
|
20
23
|
config = { ...config, ...updates };
|
|
21
24
|
debugLog(`Cloud Delegate: Configured โ ${config.endpoint}`);
|
|
22
25
|
}
|
|
@@ -116,6 +119,8 @@ export async function dispatchTask(taskId) {
|
|
|
116
119
|
}
|
|
117
120
|
// Move to history
|
|
118
121
|
taskHistory.push({ ...task });
|
|
122
|
+
if (taskHistory.length > 1000)
|
|
123
|
+
taskHistory.splice(0, taskHistory.length - 1000);
|
|
119
124
|
activeTasks.delete(taskId);
|
|
120
125
|
debugLog(`Cloud Delegate: Task ${taskId} โ ${task.status}`);
|
|
121
126
|
return task;
|
|
@@ -129,6 +134,8 @@ export function cancelTask(taskId) {
|
|
|
129
134
|
return false;
|
|
130
135
|
task.status = "cancelled";
|
|
131
136
|
taskHistory.push({ ...task });
|
|
137
|
+
if (taskHistory.length > 1000)
|
|
138
|
+
taskHistory.splice(0, taskHistory.length - 1000);
|
|
132
139
|
activeTasks.delete(taskId);
|
|
133
140
|
debugLog(`Cloud Delegate: Cancelled task ${taskId}`);
|
|
134
141
|
return true;
|
package/dist/dashboard/server.js
CHANGED
|
@@ -187,7 +187,7 @@ return false;}
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
else {
|
|
190
|
-
res.setHeader("Access-Control-Allow-Origin",
|
|
190
|
+
res.setHeader("Access-Control-Allow-Origin", `http://localhost:${PORT}`);
|
|
191
191
|
}
|
|
192
192
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
193
193
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
@@ -217,7 +217,7 @@ return false;}
|
|
|
217
217
|
loginRateLimiter.reset(clientIP);
|
|
218
218
|
res.writeHead(200, {
|
|
219
219
|
"Content-Type": "application/json",
|
|
220
|
-
"Set-Cookie": `prism_session=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${SESSION_TTL_MS / 1000}`,
|
|
220
|
+
"Set-Cookie": `prism_session=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${SESSION_TTL_MS / 1000}${(process.env.PRISM_DASHBOARD_ORIGIN?.startsWith("https://") || process.env.PRISM_DASHBOARD_SECURE) ? "; Secure" : ""}`,
|
|
221
221
|
});
|
|
222
222
|
return res.end(JSON.stringify({ ok: true }));
|
|
223
223
|
}
|
|
@@ -237,7 +237,7 @@ return false;}
|
|
|
237
237
|
res.writeHead(200, {
|
|
238
238
|
"Content-Type": "application/json",
|
|
239
239
|
// Clear the cookie on the client side
|
|
240
|
-
"Set-Cookie": `prism_session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`,
|
|
240
|
+
"Set-Cookie": `prism_session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0${(process.env.PRISM_DASHBOARD_ORIGIN?.startsWith("https://") || process.env.PRISM_DASHBOARD_SECURE) ? "; Secure" : ""}`,
|
|
241
241
|
});
|
|
242
242
|
return res.end(JSON.stringify({ ok: true }));
|
|
243
243
|
}
|
|
@@ -635,10 +635,7 @@ return false;}
|
|
|
635
635
|
// SECURITY: Allowlist of dashboard-settable keys to prevent
|
|
636
636
|
// credential overwrite (SUPABASE_KEY, STRIPE_SECRET_KEY, etc.)
|
|
637
637
|
const SETTABLE_KEYS = new Set([
|
|
638
|
-
"PRISM_STORAGE",
|
|
639
|
-
"BRAVE_API_KEY", "BRAVE_ANSWERS_API_KEY",
|
|
640
|
-
"GOOGLE_API_KEY", "VOYAGE_API_KEY",
|
|
641
|
-
"FIRECRAWL_API_KEY", "TAVILY_API_KEY",
|
|
638
|
+
"PRISM_STORAGE",
|
|
642
639
|
"embedding_provider", "embedding_model",
|
|
643
640
|
"PRISM_ENABLE_HIVEMIND", "PRISM_DARK_FACTORY_ENABLED",
|
|
644
641
|
"PRISM_TASK_ROUTER_ENABLED", "PRISM_SCHOLAR_ENABLED",
|
|
@@ -830,7 +827,7 @@ return false;}
|
|
|
830
827
|
// Only allow files from home directory, /tmp, and current working directory.
|
|
831
828
|
const resolvedPath = path.resolve(filePath);
|
|
832
829
|
const homeDir = os.homedir();
|
|
833
|
-
const allowedPrefixes = [homeDir,
|
|
830
|
+
const allowedPrefixes = [path.join(homeDir, ".prism-mcp", "imports"), os.tmpdir()];
|
|
834
831
|
const isAllowed = allowedPrefixes.some(prefix => resolvedPath.startsWith(prefix + path.sep) || resolvedPath === prefix);
|
|
835
832
|
if (!isAllowed) {
|
|
836
833
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
@@ -1362,7 +1359,8 @@ self.addEventListener('message', (e) => {
|
|
|
1362
1359
|
reject(err);
|
|
1363
1360
|
};
|
|
1364
1361
|
httpServer.on("error", onError);
|
|
1365
|
-
|
|
1362
|
+
const bindHost = AUTH_ENABLED ? "0.0.0.0" : "127.0.0.1";
|
|
1363
|
+
httpServer.listen(port, bindHost, () => {
|
|
1366
1364
|
httpServer.removeListener("error", onError);
|
|
1367
1365
|
// Re-register a permanent error handler for runtime errors
|
|
1368
1366
|
httpServer.on("error", (err) => {
|
package/dist/lifecycle.js
CHANGED
|
@@ -141,13 +141,41 @@ export function acquireLock() {
|
|
|
141
141
|
log(`Warning: Failed to process existing PID file: ${err instanceof Error ? err.message : String(err)}`);
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
-
// Claim the lock for this process
|
|
144
|
+
// Claim the lock for this process โ use 'wx' for atomic exclusive creation (prevents TOCTOU)
|
|
145
145
|
try {
|
|
146
|
-
fs.writeFileSync(PID_FILE, process.pid.toString(), "utf8");
|
|
146
|
+
fs.writeFileSync(PID_FILE, process.pid.toString(), { encoding: "utf8", flag: "wx" });
|
|
147
147
|
log(`Acquired singleton lock (PID ${process.pid})`);
|
|
148
148
|
}
|
|
149
|
-
catch (
|
|
150
|
-
|
|
149
|
+
catch (wxErr) {
|
|
150
|
+
if (wxErr.code === "EEXIST") {
|
|
151
|
+
// File was created between our check and write โ re-read and verify
|
|
152
|
+
try {
|
|
153
|
+
const racePid = parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
|
|
154
|
+
let raceAlive = false;
|
|
155
|
+
try {
|
|
156
|
+
process.kill(racePid, 0);
|
|
157
|
+
raceAlive = true;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
raceAlive = false;
|
|
161
|
+
}
|
|
162
|
+
if (!raceAlive || isOrphanProcess(racePid)) {
|
|
163
|
+
// Dead or orphan โ safe to overwrite
|
|
164
|
+
fs.writeFileSync(PID_FILE, process.pid.toString(), "utf8");
|
|
165
|
+
log(`Acquired singleton lock (PID ${process.pid}) โ stale PID ${racePid} replaced`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
log(`Existing server (PID ${racePid}) is active. Coexisting...`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (innerErr) {
|
|
173
|
+
log(`Warning: Failed to handle PID race: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
log(`Warning: Failed to write PID file: ${wxErr instanceof Error ? wxErr.message : String(wxErr)}`);
|
|
178
|
+
}
|
|
151
179
|
}
|
|
152
180
|
}
|
|
153
181
|
/**
|
|
@@ -12,7 +12,8 @@ export async function searchYahooFree(query, limit = 5) {
|
|
|
12
12
|
method: 'GET',
|
|
13
13
|
headers: {
|
|
14
14
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
15
|
-
}
|
|
15
|
+
},
|
|
16
|
+
signal: AbortSignal.timeout(15_000),
|
|
16
17
|
});
|
|
17
18
|
if (!response.ok) {
|
|
18
19
|
throw new Error(`Yahoo Search failed with status: ${response.status}`);
|
|
@@ -46,10 +47,37 @@ export async function searchYahooFree(query, limit = 5) {
|
|
|
46
47
|
* and converts it to Markdown using Turndown.
|
|
47
48
|
*/
|
|
48
49
|
export async function scrapeArticleLocal(url) {
|
|
50
|
+
// SSRF protection: reject private/internal URLs.
|
|
51
|
+
// Set PRISM_DEV_MODE=1 to allow loopback/private hosts during local dev
|
|
52
|
+
// (testing against a local docs server, internal wiki, etc.). The flag
|
|
53
|
+
// is intentionally OFF in production deploys.
|
|
54
|
+
const devMode = process.env.PRISM_DEV_MODE === '1' || process.env.NODE_ENV === 'development';
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new URL(url);
|
|
57
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
|
|
58
|
+
throw new Error('Invalid protocol');
|
|
59
|
+
const host = parsed.hostname.toLowerCase();
|
|
60
|
+
const isLoopback = host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
61
|
+
const isPrivate = host.startsWith('10.') || host.startsWith('192.168.') || host.startsWith('169.254.') ||
|
|
62
|
+
/^172\.(1[6-9]|2\d|3[01])\./.test(host) || host.endsWith('.internal') || host.endsWith('.local');
|
|
63
|
+
if (isLoopback && !devMode) {
|
|
64
|
+
throw new Error('Loopback URLs not allowed in production (set PRISM_DEV_MODE=1 to allow)');
|
|
65
|
+
}
|
|
66
|
+
if (isPrivate) {
|
|
67
|
+
// Private RFC1918 ranges are never allowed โ even in dev mode they
|
|
68
|
+
// can cross into other tenants' machines on a shared LAN.
|
|
69
|
+
throw new Error('Private network URLs not allowed');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
throw new Error(`Invalid URL: ${e instanceof Error ? e.message : 'unknown'}`);
|
|
74
|
+
}
|
|
49
75
|
const response = await fetch(url, {
|
|
50
76
|
headers: {
|
|
51
77
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
52
|
-
}
|
|
78
|
+
},
|
|
79
|
+
redirect: 'error',
|
|
80
|
+
signal: AbortSignal.timeout(15_000),
|
|
53
81
|
});
|
|
54
82
|
if (!response.ok) {
|
|
55
83
|
throw new Error(`Failed to fetch article HTML: ${response.statusText}`);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { BRAVE_API_KEY, FIRECRAWL_API_KEY,
|
|
1
|
+
import { BRAVE_API_KEY, FIRECRAWL_API_KEY, GOOGLE_SEARCH_API_KEY, GOOGLE_SEARCH_CX, SEMANTIC_SCHOLAR_API_KEY, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN, PRISM_USER_ID, PRISM_SCHOLAR_TOPICS, PRISM_ENABLE_HIVEMIND } from "../config.js";
|
|
2
2
|
import { getStorage } from "../storage/index.js";
|
|
3
3
|
import { debugLog } from "../utils/logger.js";
|
|
4
4
|
import { getLLMProvider } from "../utils/llm/factory.js";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { performWebSearchRaw } from "../utils/braveApi.js";
|
|
7
|
-
import { performTavilySearch, performTavilyExtract } from "../utils/tavilyApi.js";
|
|
8
7
|
import { performGoogleSearch } from "../utils/googleSearchApi.js";
|
|
9
8
|
import { getTracer } from "../utils/telemetry.js";
|
|
10
9
|
import { searchYahooFree, scrapeArticleLocal } from "./freeSearch.js";
|
|
@@ -91,8 +90,7 @@ export async function runWebScholar(overrideTopic, overrideProject) {
|
|
|
91
90
|
try {
|
|
92
91
|
const useGoogle = !!(GOOGLE_SEARCH_API_KEY && GOOGLE_SEARCH_CX);
|
|
93
92
|
const useBraveFirecrawl = !useGoogle && !!(BRAVE_API_KEY && FIRECRAWL_API_KEY);
|
|
94
|
-
const
|
|
95
|
-
const useFreeFallback = !useGoogle && !useBraveFirecrawl && !useTavily;
|
|
93
|
+
const useFreeFallback = !useGoogle && !useBraveFirecrawl;
|
|
96
94
|
const topic = overrideTopic || await selectTopic();
|
|
97
95
|
const project = overrideProject || SCHOLAR_PROJECT;
|
|
98
96
|
if (!topic) {
|
|
@@ -112,10 +110,6 @@ export async function runWebScholar(overrideTopic, overrideProject) {
|
|
|
112
110
|
const braveData = JSON.parse(braveResponse);
|
|
113
111
|
urls = (braveData.web?.results || []).map((r) => r.url).filter(Boolean);
|
|
114
112
|
}
|
|
115
|
-
else if (useTavily) {
|
|
116
|
-
const tavilyResults = await performTavilySearch(TAVILY_API_KEY, topic, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN);
|
|
117
|
-
urls = tavilyResults.map(r => r.url).filter(Boolean);
|
|
118
|
-
}
|
|
119
113
|
else {
|
|
120
114
|
// Parallel Academic Discovery (PubMed + ERIC + Semantic Scholar)
|
|
121
115
|
const academicCount = Math.ceil(PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN / 2);
|
|
@@ -135,21 +129,12 @@ export async function runWebScholar(overrideTopic, overrideProject) {
|
|
|
135
129
|
return `No articles found for "${topic}"`;
|
|
136
130
|
await hivemindHeartbeat(`Scraping ${urls.length} articles on: ${topic}`);
|
|
137
131
|
const scrapedTexts = [];
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
scrapedTexts.push(`Source: ${item.url}\n\n${item.rawContent.slice(0, 15_000)}`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
for (const url of urls) {
|
|
147
|
-
try {
|
|
148
|
-
const article = await scrapeArticleLocal(url);
|
|
149
|
-
scrapedTexts.push(`Source: ${url}\nTitle: ${article.title}\n\n${article.content.slice(0, 15_000)}`);
|
|
150
|
-
}
|
|
151
|
-
catch { }
|
|
132
|
+
for (const url of urls) {
|
|
133
|
+
try {
|
|
134
|
+
const article = await scrapeArticleLocal(url);
|
|
135
|
+
scrapedTexts.push(`Source: ${url}\nTitle: ${article.title}\n\n${article.content.slice(0, 15_000)}`);
|
|
152
136
|
}
|
|
137
|
+
catch { }
|
|
153
138
|
}
|
|
154
139
|
if (scrapedTexts.length === 0)
|
|
155
140
|
return "All scrapes failed";
|
|
@@ -187,7 +172,7 @@ export async function runWebScholar(overrideTopic, overrideProject) {
|
|
|
187
172
|
async function searchPubMed(query, count) {
|
|
188
173
|
const searchUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=${encodeURIComponent(query)}&retmode=json&retmax=${count}`;
|
|
189
174
|
try {
|
|
190
|
-
const searchResp = await fetch(searchUrl);
|
|
175
|
+
const searchResp = await fetch(searchUrl, { signal: AbortSignal.timeout(15_000) });
|
|
191
176
|
if (!searchResp.ok)
|
|
192
177
|
throw new Error("PubMed Search failed");
|
|
193
178
|
const searchData = await searchResp.json();
|
|
@@ -202,7 +187,7 @@ async function searchPubMed(query, count) {
|
|
|
202
187
|
async function searchERIC(query, count) {
|
|
203
188
|
const url = `https://api.ies.ed.gov/eric/?search=${encodeURIComponent(query)}&rows=${count}&format=json`;
|
|
204
189
|
try {
|
|
205
|
-
const resp = await fetch(url);
|
|
190
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(15_000) });
|
|
206
191
|
if (!resp.ok)
|
|
207
192
|
throw new Error("ERIC Search failed");
|
|
208
193
|
const data = await resp.json();
|
|
@@ -219,7 +204,7 @@ async function searchSemanticScholar(query, count) {
|
|
|
219
204
|
const headers = {};
|
|
220
205
|
if (SEMANTIC_SCHOLAR_API_KEY)
|
|
221
206
|
headers["x-api-key"] = SEMANTIC_SCHOLAR_API_KEY;
|
|
222
|
-
const resp = await fetch(url, { headers });
|
|
207
|
+
const resp = await fetch(url, { headers, signal: AbortSignal.timeout(15_000) });
|
|
223
208
|
if (!resp.ok)
|
|
224
209
|
throw new Error("Semantic Scholar Search failed");
|
|
225
210
|
const data = await resp.json();
|
package/dist/scm/client.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* SYNALUX_API_URL โ Base URL (default: https://synalux.ai)
|
|
12
12
|
* SYNALUX_API_KEY โ API key for authentication (required for paid tiers)
|
|
13
13
|
*/
|
|
14
|
+
const SAFE_SCM_NAME = /^[a-zA-Z0-9._-]+$/;
|
|
14
15
|
export class ScmClient {
|
|
15
16
|
baseUrl;
|
|
16
17
|
apiKey;
|
|
@@ -18,13 +19,20 @@ export class ScmClient {
|
|
|
18
19
|
this.baseUrl = (baseUrl || process.env.SYNALUX_API_URL || 'https://synalux.ai').replace(/\/$/, '');
|
|
19
20
|
this.apiKey = apiKey || process.env.SYNALUX_API_KEY;
|
|
20
21
|
}
|
|
22
|
+
parseRepo(repo) {
|
|
23
|
+
const [owner, name] = repo.split('/');
|
|
24
|
+
if (!owner || !name || !SAFE_SCM_NAME.test(owner) || !SAFE_SCM_NAME.test(name)) {
|
|
25
|
+
throw new Error(`Invalid repo identifier: "${repo}". Owner and name must match /^[a-zA-Z0-9._-]+$/.`);
|
|
26
|
+
}
|
|
27
|
+
return [owner, name];
|
|
28
|
+
}
|
|
21
29
|
async request(path, options) {
|
|
22
30
|
const url = `${this.baseUrl}/api/v1/scm${path}`;
|
|
23
31
|
const headers = {
|
|
24
32
|
'Content-Type': 'application/json',
|
|
25
33
|
...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
|
|
26
34
|
};
|
|
27
|
-
const res = await fetch(url, { ...options, headers: { ...headers, ...options?.headers } });
|
|
35
|
+
const res = await fetch(url, { ...options, headers: { ...headers, ...options?.headers }, signal: AbortSignal.timeout(15_000) });
|
|
28
36
|
if (!res.ok) {
|
|
29
37
|
const body = await res.text().catch(() => '');
|
|
30
38
|
throw new Error(`SCM API ${res.status}: ${body || res.statusText}`);
|
|
@@ -33,7 +41,7 @@ export class ScmClient {
|
|
|
33
41
|
}
|
|
34
42
|
// โโ Code Search โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
35
43
|
async search(repo, query) {
|
|
36
|
-
const [owner, name] =
|
|
44
|
+
const [owner, name] = this.parseRepo(repo);
|
|
37
45
|
return this.request(`/repos/${owner}/${name}/search`, {
|
|
38
46
|
method: 'POST',
|
|
39
47
|
body: JSON.stringify(query),
|
|
@@ -41,7 +49,7 @@ export class ScmClient {
|
|
|
41
49
|
}
|
|
42
50
|
// โโ AI Review โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
43
51
|
async review(repo, files, options) {
|
|
44
|
-
const [owner, name] =
|
|
52
|
+
const [owner, name] = this.parseRepo(repo);
|
|
45
53
|
return this.request(`/repos/${owner}/${name}/review`, {
|
|
46
54
|
method: 'POST',
|
|
47
55
|
body: JSON.stringify({ files, hipaa: options?.hipaa }),
|
|
@@ -49,7 +57,7 @@ export class ScmClient {
|
|
|
49
57
|
}
|
|
50
58
|
// โโ Security Scan โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
51
59
|
async scan(repo, files) {
|
|
52
|
-
const [owner, name] =
|
|
60
|
+
const [owner, name] = this.parseRepo(repo);
|
|
53
61
|
return this.request(`/repos/${owner}/${name}/security`, {
|
|
54
62
|
method: 'POST',
|
|
55
63
|
body: JSON.stringify({ files }),
|
|
@@ -57,7 +65,7 @@ export class ScmClient {
|
|
|
57
65
|
}
|
|
58
66
|
// โโ DORA Metrics โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
59
67
|
async dora(repo, period) {
|
|
60
|
-
const [owner, name] =
|
|
68
|
+
const [owner, name] = this.parseRepo(repo);
|
|
61
69
|
const qs = period ? `?period=${encodeURIComponent(period)}` : '';
|
|
62
70
|
return this.request(`/repos/${owner}/${name}/dora${qs}`);
|
|
63
71
|
}
|
package/dist/scm/githubSync.js
CHANGED
|
@@ -47,7 +47,7 @@ async function githubFetch(path, method = "GET", body) {
|
|
|
47
47
|
if (body) {
|
|
48
48
|
options.body = JSON.stringify(body);
|
|
49
49
|
}
|
|
50
|
-
const response = await fetch(url, options);
|
|
50
|
+
const response = await fetch(url, { ...options, signal: AbortSignal.timeout(15_000) });
|
|
51
51
|
const data = await response.json();
|
|
52
52
|
return { status: response.status, data };
|
|
53
53
|
}
|