prism-mcp-server 19.3.2 → 20.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.
@@ -1,22 +1,16 @@
1
1
  /**
2
- * Skill routing client — fetches the canonical routing table from synalux.
3
- *
4
- * Single source of truth lives in synalux at /api/v1/skills/routing.
5
- * This module:
6
- * 1. Caches the response in-memory for 5 minutes (matches synalux's
7
- * Cache-Control s-maxage).
8
- * 2. Falls back to a tiny offline default if synalux is unreachable, so
9
- * free-tier / disconnected installations still get the BCBA universal
10
- * skill loaded.
11
- *
12
- * To change the routing for production, edit the portal routing endpoint
13
- * and deploy. prism-mcp picks up the new config within 5 minutes.
14
- *
15
- * Do NOT add hardcoded skill names here outside the OFFLINE_FALLBACK block
16
- * — that defeats the single-source-of-truth design.
2
+ * Skill routing thin client — all routing logic is portal-side.
3
+ * POST SYNALUX_BASE/api/v1/prism/skills with bearer auth.
4
+ * Cache: keyed on (project,prompt,role), 5-min live / 30s failure.
5
+ * Offline: last-good from local DB, or empty with warning.
17
6
  */
18
- // Minimal fallback when synalux is unreachable.
19
- const OFFLINE_FALLBACK = {
7
+ // -- Constants ----------------------------------------------------------------
8
+ const SYNALUX_BASE = process.env.SYNALUX_BASE_URL || 'https://synalux.ai';
9
+ const SKILLS_TOKEN = process.env.PRISM_SKILLS_TOKEN || '';
10
+ const LIVE_TTL = 5 * 60 * 1000;
11
+ const FAIL_TTL = 30_000;
12
+ const DEFAULT_UL = { enabled: false, key_prefix: 'user_skill:' };
13
+ export const OFFLINE_FALLBACK = {
20
14
  version: 1,
21
15
  universal: [
22
16
  { name: 'prime-directive', priority: 0, protected: true },
@@ -24,144 +18,117 @@ const OFFLINE_FALLBACK = {
24
18
  { name: 'bcba_ai_assistant', priority: 20 },
25
19
  ],
26
20
  projects: {},
27
- user_local: { enabled: false, key_prefix: 'user_skill:' },
21
+ user_local: DEFAULT_UL,
28
22
  };
29
- const SYNALUX_BASE = process.env.SYNALUX_BASE_URL || 'https://synalux.ai';
30
- const LIVE_CACHE_TTL_MS = 5 * 60 * 1000; // 5min for successful fetches
31
- const FAILURE_BACKOFF_MS = 30_000; // F5 fix: 30s retry after failure (not 5min)
32
- // F5 fix: track live vs fallback separately so a transient failure doesn't
33
- // negative-cache the full routing table for 5 minutes, dropping 19 skills.
34
- let cached = null;
35
- let inflight = null;
36
- async function fetchOnce() {
23
+ const cache = new Map();
24
+ const inflightMap = new Map();
25
+ function cacheKey(project, prompt) {
26
+ return `${project}|${prompt || ''}`;
27
+ }
28
+ // Persist last-good to local DB for offline fallback
29
+ let persistFn = null;
30
+ let readFn = null;
31
+ export function _setStorage(persist, read) {
32
+ persistFn = persist;
33
+ readFn = read;
34
+ }
35
+ async function callPortal(project, prompt, role) {
37
36
  try {
38
- const res = await fetch(`${SYNALUX_BASE}/.well-known/prism/skills-routing.json`, {
39
- headers: { Accept: 'application/json' },
40
- // Routing is on every session_load_context, must not block long.
41
- signal: AbortSignal.timeout(2_500),
37
+ const body = { project };
38
+ if (prompt)
39
+ body.prompt = prompt;
40
+ if (role)
41
+ body.role = role;
42
+ const headers = {
43
+ 'Content-Type': 'application/json',
44
+ 'Accept': 'application/json',
45
+ };
46
+ if (SKILLS_TOKEN)
47
+ headers['Authorization'] = `Bearer ${SKILLS_TOKEN}`;
48
+ const res = await fetch(`${SYNALUX_BASE}/api/v1/prism/skills`, {
49
+ method: 'POST', headers, body: JSON.stringify(body),
50
+ signal: AbortSignal.timeout(5_000),
42
51
  });
43
52
  if (!res.ok)
44
53
  throw new Error(`HTTP ${res.status}`);
45
- const body = (await res.json());
46
- if (typeof body !== 'object' ||
47
- body == null ||
48
- typeof body.version !== 'number' ||
49
- !Array.isArray(body.universal) ||
50
- typeof body.projects !== 'object') {
51
- throw new Error('malformed routing table');
52
- }
53
- return body;
54
+ return (await res.json());
54
55
  }
55
56
  catch {
56
- // On failure: return the last live table (stale-while-revalidate) or OFFLINE_FALLBACK.
57
- // The caller marks this as isLive=false so it retries after FAILURE_BACKOFF_MS, not 5min.
58
- return cached?.table ?? OFFLINE_FALLBACK;
59
- }
60
- }
61
- /**
62
- * Resolve the skill list for a given project (case-insensitive substring
63
- * match against the routing table). Always returns at least the universal
64
- * skills. Also returns the user_local policy so callers know whether to
65
- * load user_skill:* entries from local SQLite.
66
- */
67
- function normalizeEntry(entry, defaultPriority) {
68
- if (typeof entry === 'string') {
69
- return { name: entry, priority: defaultPriority, protected: false };
57
+ return null;
70
58
  }
71
- return { name: entry.name, priority: entry.priority ?? defaultPriority, protected: entry.protected ?? false };
72
59
  }
73
- export async function resolveSkillsForProject(project) {
74
- const now = Date.now();
75
- // F5 fix: use shorter backoff TTL after failures so a 30s synalux hiccup doesn't
76
- // lock the session into OFFLINE_FALLBACK for 5 minutes.
77
- const ttl = (cached?.isLive ?? true) ? LIVE_CACHE_TTL_MS : FAILURE_BACKOFF_MS;
78
- if (!cached || now - cached.fetchedAt > ttl) {
79
- if (!inflight) {
80
- inflight = fetchOnce().then((table) => {
81
- // isLive = true only when we got a live response (not stale cache / OFFLINE_FALLBACK)
82
- const isLive = table !== OFFLINE_FALLBACK && table !== cached?.table;
83
- cached = { table, fetchedAt: Date.now(), isLive };
84
- return table;
85
- }).finally(() => { inflight = null; });
86
- }
87
- await inflight;
88
- }
89
- const isOffline = !(cached?.isLive ?? false);
90
- const table = cached.table;
91
- const seen = new Set();
92
- const skills = [];
93
- for (let i = 0; i < table.universal.length; i++) {
94
- const entry = normalizeEntry(table.universal[i], i);
95
- if (!seen.has(entry.name)) {
96
- seen.add(entry.name);
97
- skills.push(entry);
98
- }
99
- }
100
- const projectLower = project.toLowerCase();
101
- let projectPriority = 100;
102
- for (const [pattern, projectSkills] of Object.entries(table.projects)) {
103
- if (projectLower.includes(pattern)) {
104
- for (const s of projectSkills) {
105
- if (!seen.has(s)) {
106
- seen.add(s);
107
- skills.push({ name: s, priority: projectPriority++, protected: false });
108
- }
109
- }
110
- }
111
- }
112
- skills.sort((a, b) => a.priority - b.priority);
60
+ function makeOffline(skillBlock) {
113
61
  return {
114
- names: skills.map(s => s.name),
115
- skills,
116
- user_local: table.user_local ?? OFFLINE_FALLBACK.user_local,
117
- isOffline,
62
+ names: [], skills: [], user_local: DEFAULT_UL, isOffline: true,
63
+ skillBlock: skillBlock || '',
118
64
  };
119
65
  }
120
- /**
121
- * Resolve skills based on user prompt keywords. Matches prompt text
122
- * against the routing table's prompt_keywords regex patterns.
123
- * Returns deduplicated skill names (excluding any already in baseSkills).
124
- */
125
- export async function resolveSkillsForPrompt(prompt, baseSkills = []) {
66
+ // -- Public API ---------------------------------------------------------------
67
+ export async function resolveSkills(project, prompt, role) {
68
+ const key = cacheKey(project, prompt);
126
69
  const now = Date.now();
127
- const ttl = (cached?.isLive ?? true) ? LIVE_CACHE_TTL_MS : FAILURE_BACKOFF_MS;
128
- if (!cached || now - cached.fetchedAt > ttl) {
129
- if (!inflight) {
130
- inflight = fetchOnce().then((table) => {
131
- const isLive = table !== OFFLINE_FALLBACK && table !== cached?.table;
132
- cached = { table, fetchedAt: Date.now(), isLive };
133
- return table;
134
- }).finally(() => { inflight = null; });
70
+ const entry = cache.get(key);
71
+ const ttl = (entry?.live ?? true) ? LIVE_TTL : FAIL_TTL;
72
+ if (!entry || now - entry.at > ttl) {
73
+ if (!inflightMap.has(key)) {
74
+ const p = callPortal(project, prompt, role).then(async (r) => {
75
+ if (r) {
76
+ cache.set(key, { resp: r, at: Date.now(), live: true });
77
+ // Persist last-good for offline fallback
78
+ if (persistFn) {
79
+ try {
80
+ await persistFn(`skill_cache:${project}`, JSON.stringify(r));
81
+ }
82
+ catch { }
83
+ }
84
+ }
85
+ else if (entry) {
86
+ cache.set(key, { ...entry, at: Date.now(), live: false });
87
+ }
88
+ return r;
89
+ }).finally(() => { inflightMap.delete(key); });
90
+ inflightMap.set(key, p);
135
91
  }
136
- await inflight;
92
+ await inflightMap.get(key);
137
93
  }
138
- const table = cached.table;
139
- if (!table.prompt_keywords)
140
- return [];
141
- const existing = new Set(baseSkills);
142
- const matched = [];
143
- for (const [pattern, skills] of Object.entries(table.prompt_keywords)) {
94
+ const cached = cache.get(key);
95
+ if (cached) {
96
+ return {
97
+ names: cached.resp.loaded,
98
+ skills: cached.resp.loaded.map((name, i) => ({
99
+ name, priority: i, protected: false, category: 'universal',
100
+ })),
101
+ user_local: DEFAULT_UL,
102
+ isOffline: !cached.live,
103
+ skillBlock: cached.resp.skillBlock,
104
+ routing_version: cached.resp.routing_version,
105
+ };
106
+ }
107
+ // No cached response — try last-good from local DB
108
+ if (readFn) {
144
109
  try {
145
- const re = new RegExp(pattern, 'i');
146
- if (re.test(prompt)) {
147
- for (const s of skills) {
148
- if (!existing.has(s)) {
149
- existing.add(s);
150
- matched.push(s);
151
- }
152
- }
110
+ const stored = await readFn(`skill_cache:${project}`);
111
+ if (stored) {
112
+ const resp = JSON.parse(stored);
113
+ return {
114
+ names: resp.loaded, skills: [], user_local: DEFAULT_UL,
115
+ isOffline: true, skillBlock: resp.skillBlock,
116
+ routing_version: resp.routing_version,
117
+ };
153
118
  }
154
119
  }
155
- catch {
156
- // Invalid regex in routing table — skip silently
157
- }
120
+ catch { }
158
121
  }
159
- return matched;
122
+ return makeOffline();
123
+ }
124
+ export async function resolveSkillsForProject(project) {
125
+ return resolveSkills(project);
126
+ }
127
+ export async function resolveSkillsForPrompt(_prompt, _baseSkills = []) {
128
+ return [];
160
129
  }
161
- /** Force a re-fetch on the next call. Exposed for tests + admin tooling. */
162
130
  export function _invalidateRoutingCache() {
163
- cached = null;
164
- inflight = null;
131
+ cache.clear();
132
+ inflightMap.clear();
165
133
  }
166
- /** Test/debug only — read the OFFLINE_FALLBACK constant. */
167
134
  export const _OFFLINE_FALLBACK = OFFLINE_FALLBACK;
@@ -1,129 +1,52 @@
1
- /**
2
- * Voyage AI Adapter (v1.0)
3
- * ─────────────────────────────────────────────────────────────────────────────
4
- * PURPOSE:
5
- * Implements LLMProvider using Voyage AI's REST API for text embeddings.
6
- * Voyage AI is the embedding provider officially recommended by Anthropic
7
- * for use alongside Claude — it fills the gap left by Anthropic's lack
8
- * of a native embedding API.
9
- *
10
- * TEXT GENERATION:
11
- * Voyage AI is an embeddings-only service. generateText() throws an explicit
12
- * error, the same pattern used by AnthropicAdapter.generateEmbedding().
13
- * Set text_provider separately (anthropic, openai, or gemini).
14
- *
15
- * EMBEDDING DIMENSION PARITY (768 dims):
16
- * Prism's SQLite (sqlite-vec) and Supabase (pgvector) schemas define
17
- * embedding columns as EXACTLY 768 dimensions.
18
- *
19
- * Voyage solution: voyage-3 and voyage-3-lite output 1024 dims by default,
20
- * but both support the `output_dimension` parameter (Matryoshka Representation
21
- * Learning), enabling truncation to 768 while preserving quality.
22
- * voyage-3-lite at 768 dims is the fastest and most cost-efficient option.
23
- *
24
- * MODELS:
25
- * voyage-3 — Highest quality, 1024 dims natively (MRL → 768)
26
- * voyage-3-lite — Fast & cheap, 512 dims natively (MRL → 768 NOT supported)
27
- * voyage-3-large — Best quality, use for offline indexing
28
- * voyage-code-3 — Optimised for code (recommended for dev sessions)
29
- *
30
- * NOTE: voyage-3-lite natively outputs 512 dims; it does NOT support
31
- * output_dimension truncation to 768. Use voyage-3 for dimension parity.
32
- * Default is voyage-3 for this reason.
33
- *
34
- * CONFIG KEYS (Prism dashboard "AI Providers" tab OR environment variables):
35
- * voyage_api_key — Required. Voyage AI API key (pa-...)
36
- * voyage_model — Embedding model (default: voyage-3)
37
- *
38
- * USAGE WITH ANTHROPIC TEXT PROVIDER:
39
- * Set text_provider=anthropic, embedding_provider=voyage in the dashboard.
40
- * This pairs Claude for reasoning with Voyage for semantic memory — the
41
- * combination Anthropic recommends in their documentation.
42
- *
43
- * API REFERENCE:
44
- * https://docs.voyageai.com/reference/embeddings-api
45
- */
46
1
  import { getSettingSync } from "../../../storage/configStorage.js";
47
2
  import { debugLog } from "../../logger.js";
48
- // ─── Constants ────────────────────────────────────────────────────────────────
49
- // Must match Prism's DB schema (sqlite-vec and pgvector column sizes).
50
3
  const EMBEDDING_DIMS = 768;
51
- // voyage-3 supports up to 32,000 tokens. Character-based cap (consistent
52
- // with OpenAI and Gemini adapters) avoids tokenizer dependency.
53
- // 8000 chars ≈ 1500-2000 tokens for typical session summaries.
54
- const MAX_EMBEDDING_CHARS = 8000;
55
- // Default model: voyage-3 (supports output_dimension=768 via MRL)
56
- // voyage-3-lite is NOT recommended as its native 512 dims < 768.
57
- const DEFAULT_MODEL = "voyage-3";
58
- const VOYAGE_API_BASE = "https://api.voyageai.com/v1";
59
- // ─── Adapter ─────────────────────────────────────────────────────────────────
60
- export class VoyageAdapter {
4
+ const MAX_EMBEDDING_CHARS = 120_000;
5
+ const TIMEOUT_MS = 30_000;
6
+ const API_URL = "https://api.voyageai.com/v1/embeddings";
7
+ export { VoyageEmbeddingAdapter as VoyageAdapter };
8
+ export class VoyageEmbeddingAdapter {
61
9
  apiKey;
62
10
  constructor() {
63
- const apiKey = getSettingSync("voyage_api_key", process.env.VOYAGE_API_KEY ?? "");
64
- if (!apiKey) {
65
- throw new Error("VoyageAdapter requires a Voyage AI API key. " +
66
- "Get one free at https://dash.voyageai.com — then set VOYAGE_API_KEY " +
67
- "or configure it in the Mind Palace dashboard under 'AI Providers'.");
11
+ const key = getSettingSync("voyage_api_key", process.env.VOYAGE_API_KEY ?? "");
12
+ if (!key) {
13
+ throw new Error("Voyage AI API key is required — set voyage_api_key in settings or VOYAGE_API_KEY env var");
68
14
  }
69
- this.apiKey = apiKey;
70
- debugLog("[VoyageAdapter] Initialized");
15
+ this.apiKey = key;
71
16
  }
72
- // ─── Text Generation (Not Supported) ────────────────────────────────────
73
17
  async generateText(_prompt, _systemInstruction) {
74
- // Voyage AI is an embeddings-only service.
75
- // Use text_provider=anthropic, openai, or gemini for text generation.
76
- throw new Error("VoyageAdapter does not support text generation. " +
77
- "Voyage AI is an embeddings-only service. " +
78
- "Set text_provider to 'anthropic', 'openai', or 'gemini' in the dashboard.");
18
+ throw new Error("VoyageEmbeddingAdapter only supports embeddings — use a different provider for text generation");
79
19
  }
80
- // ─── Embedding Generation ────────────────────────────────────────────────
81
20
  async generateEmbedding(text) {
82
- if (!text || !text.trim()) {
83
- throw new Error("[VoyageAdapter] generateEmbedding called with empty text");
21
+ const trimmed = text.trim();
22
+ if (!trimmed) {
23
+ throw new Error("Cannot generate embedding for empty or whitespace-only input");
84
24
  }
85
- // Truncate to character limit (consistent with other adapters)
86
- const truncated = text.length > MAX_EMBEDDING_CHARS
87
- ? text.slice(0, MAX_EMBEDDING_CHARS).replace(/\s+\S*$/, "")
88
- : text;
89
- const model = getSettingSync("voyage_model", DEFAULT_MODEL);
90
- debugLog(`[VoyageAdapter] generateEmbedding — model=${model}, chars=${truncated.length}`);
91
- const requestBody = {
92
- input: [truncated],
93
- model,
94
- // Request exactly 768 dims via Matryoshka truncation.
95
- // Supported by voyage-3, voyage-3-large, voyage-code-3.
96
- // voyage-3-lite (native 512 dims) will ignore this and return 512,
97
- // which will be caught by the dimension guard below.
98
- output_dimension: EMBEDDING_DIMS,
99
- };
100
- const response = await fetch(`${VOYAGE_API_BASE}/embeddings`, {
25
+ const input = trimmed.length > MAX_EMBEDDING_CHARS ? trimmed.slice(0, MAX_EMBEDDING_CHARS) : trimmed;
26
+ const model = getSettingSync("voyage_model", "voyage-3.5");
27
+ const response = await fetch(API_URL, {
101
28
  method: "POST",
102
29
  headers: {
103
- "Authorization": `Bearer ${this.apiKey}`,
104
30
  "Content-Type": "application/json",
31
+ "Authorization": `Bearer ${this.apiKey}`,
105
32
  },
106
- body: JSON.stringify(requestBody),
33
+ body: JSON.stringify({
34
+ input,
35
+ model,
36
+ input_type: "document",
37
+ output_dimension: EMBEDDING_DIMS,
38
+ truncation: true,
39
+ }),
40
+ signal: AbortSignal.timeout(TIMEOUT_MS),
107
41
  });
108
42
  if (!response.ok) {
109
- const errorText = await response.text().catch(() => "unknown error");
110
- throw new Error(`[VoyageAdapter] API request failed — status=${response.status}: ${errorText}`);
111
- }
112
- const data = (await response.json());
113
- const embedding = data?.data?.[0]?.embedding;
114
- if (!Array.isArray(embedding)) {
115
- throw new Error("[VoyageAdapter] Unexpected response format — no embedding array found");
43
+ throw new Error(`Voyage AI API error: HTTP ${response.status}`);
116
44
  }
117
- // Dimension guard: Prism's DB schema requires exactly 768 dims.
118
- // This catches voyage-3-lite (512) or future API changes silently early.
45
+ const json = await response.json();
46
+ const embedding = json.data[0].embedding;
119
47
  if (embedding.length !== EMBEDDING_DIMS) {
120
- throw new Error(`[VoyageAdapter] Embedding dimension mismatch: expected ${EMBEDDING_DIMS}, ` +
121
- `got ${embedding.length}. ` +
122
- `Use voyage-3 (not voyage-3-lite) to get 768-dim output via MRL truncation. ` +
123
- `Change voyage_model in the Mind Palace dashboard.`);
48
+ debugLog(`[voyage] Expected ${EMBEDDING_DIMS} dimensions but got ${embedding.length}`);
124
49
  }
125
- debugLog(`[VoyageAdapter] Embedding generated — dims=${embedding.length}, ` +
126
- `tokens_used=${data.usage?.total_tokens ?? "unknown"}`);
127
50
  return embedding;
128
51
  }
129
52
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * LLM Provider Factory (v4.5Voyage AI Embedding Support)
2
+ * LLM Provider Factory (v5split text + embedding providers)
3
3
  * ─────────────────────────────────────────────────────────────────────────────
4
4
  * PURPOSE:
5
5
  * Single point of resolution for the active LLMProvider.
@@ -16,16 +16,16 @@
16
16
  * When embedding_provider = "auto":
17
17
  * * If text_provider is gemini or openai → use same provider for embeddings
18
18
  * * If text_provider is anthropic → auto-fallback to gemini for embeddings
19
- * (Anthropic has no native embedding API; consider setting
20
- * embedding_provider=voyage for the Anthropic-recommended pairing)
19
+ * (Anthropic has no native embedding API; set embedding_provider=voyage
20
+ * to use Voyage AI embeddings alongside Claude)
21
21
  *
22
22
  * EXAMPLE CONFIGURATIONS:
23
23
  * text_provider=gemini, embedding_provider=auto → Gemini+Gemini (default)
24
24
  * text_provider=openai, embedding_provider=auto → OpenAI+OpenAI
25
25
  * text_provider=anthropic, embedding_provider=auto → Claude+Gemini (auto-bridge)
26
- * text_provider=anthropic, embedding_provider=voyage → Claude+Voyage (Anthropic-recommended)
26
+ * text_provider=anthropic, embedding_provider=voyage → Claude + Voyage AI embeddings
27
27
  * text_provider=anthropic, embedding_provider=openai → Claude+Ollama (cost-optimized)
28
- * text_provider=gemini, embedding_provider=voyage → Gemini+Voyage (mixed)
28
+ * text_provider=gemini, embedding_provider=voyage → Gemini + Voyage AI embeddings
29
29
  *
30
30
  * SINGLETON + GRACEFUL DEGRADATION:
31
31
  * Same as before — instance cached per process, errors fall back to Gemini.
@@ -43,7 +43,7 @@ import { getSettingSync } from "../../storage/configStorage.js";
43
43
  import { GeminiAdapter } from "./adapters/gemini.js";
44
44
  import { OpenAIAdapter } from "./adapters/openai.js";
45
45
  import { AnthropicAdapter } from "./adapters/anthropic.js";
46
- import { VoyageAdapter } from "./adapters/voyage.js";
46
+ import { VoyageAdapter } from "./adapters/voyage.js"; // clean-room rewrite
47
47
  import { LocalEmbeddingAdapter } from "./adapters/local.js";
48
48
  import { DisabledTextAdapter } from "./adapters/disabledText.js";
49
49
  import { TracingLLMProvider } from "./adapters/traced.js";
@@ -66,10 +66,10 @@ function buildEmbeddingAdapter(type) {
66
66
  // Note: "anthropic" is intentionally absent from this switch.
67
67
  // Anthropic has no embedding API, so it can never be an embedding provider.
68
68
  // The factory resolves "auto" away from "anthropic" before calling this.
69
- // For Anthropic text users, "voyage" is the Anthropic-recommended pairing.
69
+ // Anthropic users: set embedding_provider=voyage to use Voyage AI.
70
70
  switch (type) {
71
71
  case "openai": return new OpenAIAdapter();
72
- case "voyage": return new VoyageAdapter();
72
+ case "voyage": return new VoyageAdapter(); // clean-room adapter
73
73
  case "local": return new LocalEmbeddingAdapter();
74
74
  case "gemini":
75
75
  default: return new GeminiAdapter();
@@ -124,9 +124,8 @@ export function getLLMProvider() {
124
124
  if (textType === "anthropic") {
125
125
  console.info("[LLMFactory] text_provider=anthropic with embedding_provider=auto: " +
126
126
  "routing embeddings to GeminiAdapter (Anthropic has no native embedding API). " +
127
- "For the Anthropic-recommended pairing, set embedding_provider=voyage in the dashboard " +
128
- "(voyage-3 supports 768-dim output via MRL). " +
129
- "Alternatively, set embedding_provider=openai to use Ollama/OpenAI.");
127
+ "To use Voyage AI embeddings, set embedding_provider=voyage in the dashboard. " +
128
+ "Alternatively, set embedding_provider=openai for Ollama/OpenAI.");
130
129
  }
131
130
  }
132
131
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "19.3.2",
3
+ "version": "20.0.0",
4
4
  "mcpName": "io.github.dcostenco/prism-coder",
5
5
  "description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, PHI Guard, Tier Enforcement, Prompt-Based Skill Routing, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local-first storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B–32B open-weights LLM fleet.",
6
6
  "module": "index.ts",
@@ -97,7 +97,7 @@
97
97
  "url": "git+https://github.com/dcostenco/prism-coder.git"
98
98
  },
99
99
  "author": "Dmitri Costenco",
100
- "license": "AGPL-3.0",
100
+ "license": "Apache-2.0",
101
101
  "devDependencies": {
102
102
  "@huggingface/transformers": "3.1.0",
103
103
  "@types/bun": "latest",