prism-mcp-server 20.22.2 → 20.22.4

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 CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  **Give your AI agent memory that lasts — and see the cloud tokens it never had to spend.** Persistent sessions, knowledge graphs, offline tool-routing, and an auditable savings meter. Fully local and free.
4
4
 
5
+ > **Current release: v20.22.4 — oMLX setup and local model roles**
6
+ > Install a pinned oMLX server/model pack from the dashboard or CLI, or connect
7
+ > an existing server. Discover installed models and choose coder/reviewer roles
8
+ > for oMLX and Ollama. Controlled oMLX coding remains text-only.
9
+
5
10
  [![npm](https://img.shields.io/npm/v/prism-mcp-server?color=cb0000&label=npm)](https://www.npmjs.com/package/prism-mcp-server)
6
11
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-00ADD8)](https://github.com/modelcontextprotocol/servers)
7
12
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
@@ -22,9 +27,10 @@ A paid subscription adds cloud sync, higher model tiers, and team features throu
22
27
 
23
28
  - **Session memory that survives restarts** — resume projects with handoff notes,
24
29
  recent work, open TODOs, and configurable quick, standard, or deep context.
25
- - **Local-first inference** — bounded work is routed through local Ollama models
26
- first, with automatic 2B/4B/9B/27B selection based on installed models,
27
- available RAM, context fit, and subscription entitlements.
30
+ - **Local-first inference** — local Ollama models use automatic 2B/4B/9B/27B
31
+ selection based on installed models, available RAM, context fit, and
32
+ subscription entitlements. Eligible text-only coding can prefer configured
33
+ oMLX; explicit task choices override the saved backend preference.
28
34
  - **A savings meter you can audit** — `prism savings` (or the `local_savings`
29
35
  tool from any host) reports the token volume local serving kept off your
30
36
  cloud model: headline, local share, per-model breakdown. It reports tokens,
@@ -59,70 +65,17 @@ A paid subscription adds cloud sync, higher model tiers, and team features throu
59
65
  never sent to the cloud), and local/cloud usage is recorded for
60
66
  review.
61
67
 
62
- ### Explicit on-device oMLX drafts
63
-
64
- `prism_infer` keeps Ollama's four-tier routing by default. For an explicitly
65
- selected text-only code or advisory reviewer draft, pass `local_backend: "omlx"`,
66
- `mode: "code"`, and optionally `local_model` with the exact advertised model ID.
67
- This path authenticates `/v1/models` before classification or any Ollama unload;
68
- an unavailable, unauthenticated, or mismatched model refuses without substitution.
69
- It reads `OMLX_API_KEY` or the local `~/.omlx/settings.json` key, and model/context
70
- settings from `~/.omlx/model_settings.json`; `OMLX_MODEL` and a loopback-only
71
- `OMLX_BASE_URL` can select local configuration. Redirects are refused.
72
-
73
- **Requires** an oMLX build with Prism's control routes: the
74
- [`prism-v0.7.1rc1` branch of dcostenco/omlx](https://github.com/dcostenco/omlx/tree/prism-v0.7.1rc1), upstream oMLX
75
- 0.7.1rc1 plus those routes ([install and settings](https://github.com/dcostenco/omlx/blob/prism-v0.7.1rc1/PRISM.md)). With stock
76
- oMLX every call is refused. Generation runs as an owned
77
- job through those routes, so a timeout or cancel is reported only once the
78
- server confirms the request stopped. An unconfirmed stop refuses later calls
79
- (`omlx_stop_unproved`, `omlx_provider_busy`).
80
-
81
- Before classification, Prism unloads every model Ollama has loaded
82
- (`keep_alive: 0`; the weights stay on disk and reload on the next Ollama call)
83
- and waits up to 10 seconds for Ollama to report none. A failed unload, a model
84
- that does not drain, or one loading meanwhile refuses the call; a newcomer is
85
- never unloaded. Ollama must be reachable for this step. An OS-owned loopback
86
- lease on port 19438 excludes other candidate MCP processes; ordinary calls within
87
- one process share this lease. A busy lease refuses (`omlx_resource_busy`) before
88
- classification or unloading, and process exit releases it without lock-file
89
- cleanup. In-process inference and classification wait while an oMLX call owns
90
- the device. Unknown Ollama state, newly loaded Ollama weights, or available
91
- memory below a 2 GiB reserve aborts/withholds the generation. Model caches and
92
- daemons remain in place. Legacy/unmodified MCP processes do not honor the lease;
93
- their contention is observed and rejected by the watcher.
94
-
95
- **Safety screen.** The classifier on this path is the public
96
- `dcostenco/prism-coder-9b-mlx-8bit`, installed under the name
97
- `prism-coder-9b-mlx-8bit` in oMLX's model folder (about 9.2 GB while loaded).
98
- Layer 1's rules only refuse here; the classifier reads everything else, and a
99
- prompt over 4,000 characters is read whole in 3,600-character windows (the most
100
- severe verdict stands), never as an excerpt. A classifier failure, an unreadable
101
- answer, or a missing classifier refuses the call; there is no keyword or Ollama
102
- fallback. Each classifier read is bounded by 90 seconds and by the call's
103
- remaining `timeout_ms`, so a classifier that macOS paged out during a long idle
104
- is waited for rather than refused, and an oversize prompt read in windows
105
- cannot outlast the call budget; the first unrecoverable read ends the
106
- classification.
107
-
108
- Configured oMLX sampling and reasoning are preserved unless `temperature` or
109
- `think` is explicit, including through repairs. Generic thinking-disable retries
110
- are disabled for oMLX, so a model with no thinking budget in oMLX's model
111
- settings can spend its whole answer reasoning and is refused `think_only`: give
112
- it a budget there, or pass `think: false`. The default 300-second budget bounds
113
- oMLX generation, repairs, and history answer checks; `timeout_ms` may override it
114
- up to 900 seconds. Context sizing uses conservative UTF-8 bytes and the
115
- configured window.
116
-
117
- The same entitlement, reserved-content, history, quality, output-safety, and
118
- verification policies apply. Explicit oMLX never falls back to Ollama/cloud
119
- inference. `cloud_fallback: false`, `verify: false`, and `route_guard: "local"`
120
- keep content-bearing cloud channels disabled for a local draft. Result metadata
121
- reports `backend: "omlx"`, the selected model, tokens, and `gate_outcome`.
122
- The MCP envelope carries this public metadata in `structuredContent` alongside
123
- the existing human header/output text; `next_step` remains available in both.
124
- Runtime authentication and internal verifier traces are excluded.
125
- A draft is advisory and requires independent host review; it is not final sign-off.
68
+ ### On-device oMLX coding
69
+
70
+ On Apple Silicon, choose **Settings → Local Coding** in `prism dashboard` to
71
+ install oMLX and its model pack or use an existing server. Auto prefers configured
72
+ oMLX for eligible coding; a saved preference or explicit task choice can select
73
+ Ollama instead. This path requires
74
+ Prism's oMLX integration and its separate classifier; stock oMLX is insufficient.
75
+ See [oMLX setup, tested models, results, and failure handling](docs/omlx.md), or
76
+ start with the [quickstart beside Ollama](#optional-omlx-for-local-coding).
77
+ Explicit oMLX never falls back to Ollama or cloud inference; drafts still need
78
+ host review and tests.
126
79
 
127
80
  ## Get started
128
81
 
@@ -235,6 +188,41 @@ or by re-enabling after each run.
235
188
  <details>
236
189
  <summary>Release history (optional)</summary>
237
190
 
191
+ ## What's New in v20.22.4
192
+
193
+ - **Dashboard and CLI setup:** Settings → Local Coding and `prism local` install
194
+ a pinned oMLX server/model pack with progress, retry and managed startup, or
195
+ connect an existing server. See the [oMLX guide](docs/omlx.md) for requirements.
196
+ - **Saved coding preference:** choose Auto, oMLX or Ollama without restarting
197
+ Prism. Auto prefers configured oMLX for eligible text-only coding; an explicit
198
+ per-task backend overrides that preference.
199
+ - **Discovered model roles:** select installed coder and reviewer models for
200
+ each backend, including the same model for both roles. Explicit task model
201
+ choices win. Reported sizes and conservative memory estimates help compare
202
+ choices; unknown metadata stays unknown and estimates do not guarantee fit.
203
+ - **Managed isolation:** model discovery, Hub cache and cluster launchers stay
204
+ private to the managed installation. Competing Ollama residency refuses the
205
+ call without unloading other jobs.
206
+ - **Setup-state hardening:** bounded reads use one file handle and reject
207
+ symlinks, closing the filesystem race reported by CodeQL.
208
+ - **Vision boundary:** this release does not enable image requests through the
209
+ controlled oMLX coding path. Picture-recognition tests remain separate work.
210
+
211
+ ## What's New in v20.22.3
212
+
213
+ - **Dependency fixes:** Hono 4.13.11 addresses its static-path middleware
214
+ bypass advisory; Vite 8.2.4 addresses its development editor-launch advisories.
215
+ - **Local resource probes use numeric loopback by default.** The default
216
+ Ollama endpoint is now `http://127.0.0.1:11434`, so a stalled `localhost`
217
+ DNS lookup cannot block those probes. Explicit URL overrides are preserved;
218
+ IPv6-only servers need an explicit URL.
219
+ - **Read-only oMLX inspection accepts release-candidate versions.** Valid
220
+ `rcN` labels no longer invalidate status. Inspection still reports
221
+ generation disabled and the provider not qualified; it is not an activation
222
+ or a generation check.
223
+ - **Stable oMLX setup:** the current setup link uses the Prism integration
224
+ based on upstream `v0.7.1`. Stock oMLX still lacks the required control routes.
225
+
238
226
  ## What's New in v20.22.2
239
227
 
240
228
  **Why it matters:** an agent that followed `session_task_route`'s advice was
@@ -1333,6 +1321,32 @@ requests are refused rather than answered blind when no tier (or the Layer 1
1333
1321
  safety classifier) can actually see the image, so a text-only model is never
1334
1322
  handed a prompt about a screenshot it never received. The 27b is text only.
1335
1323
 
1324
+ ### Optional oMLX for local coding
1325
+
1326
+ Use **Settings → Local Coding** in `prism dashboard` for opt-in installation,
1327
+ progress, retry, connection checks, and backend/model selection. Or use
1328
+ `prism local install` followed by `prism local start`. npm installation never
1329
+ downloads models automatically. The managed pack requires Apple Silicon,
1330
+ macOS 15+, Python 3.11–3.13, 32 GiB memory and 40 GiB free disk; it keeps existing
1331
+ servers and jobs running. Auto prefers configured oMLX for eligible coding;
1332
+ explicit smaller model ceilings stay on Ollama, and task overrides take priority.
1333
+
1334
+ For manual setup, install the **Prism integration** of oMLX, its required
1335
+ 9B classifier, and a drafting model such as Qwen3.8-27B oQ4e. Set a loopback API
1336
+ key, enable chunked prefill, disable MTP/DFlash/speculative prefill, and configure
1337
+ model context/thinking budgets. Then verify a call with `local_backend: "omlx"`,
1338
+ `mode: "code"`, and the exact advertised `local_model`. Ollama need not be
1339
+ running; Prism refuses device contention without unloading another job's models.
1340
+
1341
+ Follow the [step-by-step oMLX guide](docs/omlx.md) for pinned install/download
1342
+ commands, server startup, a first MCP call, and troubleshooting. Its
1343
+ [tested model table](docs/omlx.md#tested-models-and-trade-offs) separates native
1344
+ oMLX checkpoints from the experimental Strata GGUF runtime. The
1345
+ [coding and review results](docs/omlx.md#observed-coding-and-review-results)
1346
+ show measured trade-offs and limits; they do not establish an overall winner.
1347
+ Stock oMLX cannot serve this Prism path, and explicit oMLX has no inference
1348
+ fallback. Local reviews remain advisory.
1349
+
1336
1350
  ---
1337
1351
 
1338
1352
  ## What it does
@@ -1483,6 +1497,9 @@ and `verify` govern the other two channels.
1483
1497
 
1484
1498
  ## Models
1485
1499
 
1500
+ This section describes the default **Ollama fleet**. For explicit oMLX coding,
1501
+ see [tested oMLX models and trade-offs](docs/omlx.md#tested-models-and-trade-offs).
1502
+
1486
1503
  The `prism-coder` fleet uses Qwen3.5 for MCP tool-routing AND general inference. The 9B and 27B are fine-tuned; the 2B and 4B use stock Qwen3.5-4B at different quantization levels. The 27B scored 100% on our internal 115-case tool-routing suite and 100% on an internal 15-problem coding eval, at $0 inference cost. These are self-run evaluations, not [BFCL](https://gorilla.cs.berkeley.edu/leaderboard.html) leaderboard submissions.
1487
1504
 
1488
1505
  `prism_infer` supports three modes: `route` (tool routing, fast), `chat` (conversation) and `code` (code generation). Reasoning is decided by the **tier**, not the mode: a tier carrying `MODEL_TIERS.prefersThinking` also carries a `minLocalTokens` floor so reasoning cannot crowd out the answer, and only those tiers use `<think>` blocks (stripped before the response is served). The 9B does; the 4B and 2B do not, because on those tiers reasoning drew down the same `num_predict` budget the answer needed and returned an empty response. An explicit `think: true` still overrides, for a caller who has sized `max_tokens` for it. If the local model fails a quality gate (empty, think-only, or truncated), paid tiers automatically escalate to Gemini 3.6 Flash via the Synalux portal.
@@ -1553,7 +1570,7 @@ selection.
1553
1570
 
1554
1571
  | Layer | What | Model | Cost |
1555
1572
  |---|---|---|---|
1556
- | **L1** | Crisis/medical safety gate | None (regex) | 0 ms |
1573
+ | **L1** | Crisis/medical safety rules and local classification | Backend-specific classifier (required 9B on oMLX) | Varies |
1557
1574
  | **L3-Registry** | Envelope validation + advertised-tool enforcement (all tiers) | None | 0 ms |
1558
1575
  | **L3-Route** | Authenticated deterministic route correction (Standard+) | None | Network latency |
1559
1576
  | **L3-Tier0** | Integer grounding (set membership) | None (deterministic) | 0 ms |
@@ -1598,10 +1615,14 @@ check, the local 9B passed 2/3 tasks; the local 27B and Gemini 3.6 Flash each
1598
1615
  passed 3/3. This is a self-published regression signal, not an independent
1599
1616
  leaderboard or a claim of broad model equivalence.
1600
1617
 
1618
+ For the separate October 2026 coding and advisory-review comparison, see
1619
+ [oMLX and Strata results and limitations](docs/omlx.md#observed-coding-and-review-results).
1620
+
1601
1621
  ### Cloud Escalation (`cloud_fallback`)
1602
1622
 
1603
- Prism always tries an eligible local model first. If the quality gate detects
1604
- an empty, truncated, think-only, or looping response, or the safety screen
1623
+ For the default **Ollama backend**, Prism tries an eligible local model first.
1624
+ If the quality gate detects an empty, truncated, think-only, or looping
1625
+ response, or the safety screen
1605
1626
  finds the request uncertain or reserved, a paid plan escalates the request
1606
1627
  through Gemini 3.6 Flash. Free-tier routing stays local and reports the
1607
1628
  outcome without making a cloud call.
@@ -1612,6 +1633,9 @@ for a call, which the clinical delegation rules do for drafting; pass `true`
1612
1633
  to ask for it, which still requires a plan with cloud. A request that carries
1613
1634
  an image is never escalated; screenshots stay on this device.
1614
1635
 
1636
+ Explicit `local_backend: "omlx"` never falls back to cloud inference or Ollama,
1637
+ regardless of plan or `cloud_fallback`. See [oMLX behavior](docs/omlx.md#runtime-behavior-and-failure-handling).
1638
+
1615
1639
  ---
1616
1640
 
1617
1641
  ## Why Prism Coder
package/dist/cli.js CHANGED
@@ -17,6 +17,48 @@ import { isRecoverableStartupStorageError } from './utils/startupRecovery.js';
17
17
  import { verifyBehaviorHandler } from './tools/behavioralVerifierHandler.js';
18
18
  import { dashboardOpenSuccessMessage, findRunningDashboardAccessState, openDashboardUrl, } from './dashboard/dashboardAccess.js';
19
19
  const program = new Command();
20
+ const local = program.command('local').description('Local coding setup and backend preference');
21
+ local.command('status').description('Show oMLX installation and saved coding preference').action(async () => {
22
+ const { omlxSetup } = await import('./utils/omlxSetup.js');
23
+ const { readLocalCodingSettings } = await import('./utils/localCodingSettings.js');
24
+ console.log(JSON.stringify({ settings: await readLocalCodingSettings(), managed: await omlxSetup.status() }, null, 2));
25
+ });
26
+ local.command('install').description('Opt in to isolated oMLX + pinned classifier/coder downloads (about 25 GB; 40 GiB free required)').action(async () => {
27
+ const { omlxSetup } = await import('./utils/omlxSetup.js');
28
+ try {
29
+ const job = await omlxSetup.install();
30
+ console.log(`oMLX setup ${job.id} started. Follow progress in Dashboard → Account & Settings → Local Coding.`);
31
+ await omlxSetup.waitForInstall();
32
+ const state = await omlxSetup.status();
33
+ if (state.job?.state !== 'complete')
34
+ throw new Error(state.job?.error ?? 'oMLX setup did not complete.');
35
+ console.log('Installed. Run prism local start, then select Prism-managed installation in the Local Coding dashboard.');
36
+ }
37
+ catch (error) {
38
+ console.error(error instanceof Error ? error.message : 'oMLX setup failed.');
39
+ process.exitCode = 1;
40
+ }
41
+ });
42
+ local.command('start').description('Start the installed managed oMLX server without replacing an existing daemon').action(async () => {
43
+ const { omlxSetup } = await import('./utils/omlxSetup.js');
44
+ try {
45
+ console.log(JSON.stringify(await omlxSetup.start()));
46
+ }
47
+ catch (error) {
48
+ console.error(error instanceof Error ? error.message : 'Unable to start oMLX.');
49
+ process.exitCode = 1;
50
+ }
51
+ });
52
+ local.command('backend <backend>').description('Save auto, omlx or ollama for coding; explicit tasks override it').action(async (backend) => {
53
+ const { readLocalCodingSettings, saveLocalCodingSettings, isLocalCodingBackend } = await import('./utils/localCodingSettings.js');
54
+ if (!isLocalCodingBackend(backend)) {
55
+ console.error('Backend must be auto, omlx or ollama.');
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+ await saveLocalCodingSettings({ ...await readLocalCodingSettings(), backend });
60
+ console.log(`Saved coding preference: ${backend}. Applies to new tasks; explicit task choices override it.`);
61
+ });
20
62
  /** Build the stable `prism load --json` envelope from depth-specific context. */
21
63
  export function buildLoadJsonOutput(project, data, level, metadata) {
22
64
  const filteredData = filterPrismMemoryContext(data);
package/dist/config.js CHANGED
@@ -345,15 +345,16 @@ export const PRISM_TURBOQUANT_TIEBREAKER_EPSILON = Number.isFinite(rawTiebreaker
345
345
  //
346
346
  // Set PRISM_LOCAL_LLM_ENABLED=true to activate.
347
347
  // Set PRISM_LOCAL_LLM_MODEL to override the model tag.
348
- // Set PRISM_LOCAL_LLM_URL to override the Ollama endpoint (default: localhost:11434).
348
+ // Set PRISM_LOCAL_LLM_URL to override the Ollama endpoint (default: 127.0.0.1:11434).
349
349
  // Set PRISM_LOCAL_LLM_TIMEOUT_MS to override per-call timeout (default: 60000, max: 300000).
350
350
  // Set PRISM_STRICT_LOCAL_MODE=true to block cloud fallback when local LLM is enabled (HIPAA).
351
351
  /** Master switch — enables the local prism-coder LLM for background tasks. */
352
352
  export const PRISM_LOCAL_LLM_ENABLED = process.env.PRISM_LOCAL_LLM_ENABLED === "true"; // Opt-in, default false
353
353
  /** Ollama model tag to use for local LLM calls. */
354
354
  export const PRISM_LOCAL_LLM_MODEL = (process.env.PRISM_LOCAL_LLM_MODEL || "prism-coder:9b").trim();
355
- /** Ollama base URL. Override for remote Ollama instances. */
356
- export const PRISM_LOCAL_LLM_URL = (process.env.PRISM_LOCAL_LLM_URL || "http://localhost:11434").trim();
355
+ /** Numeric loopback avoids queuing local health probes behind getaddrinfo work.
356
+ * Explicit overrides still support custom hostnames and IPv6-only listeners. */
357
+ export const PRISM_LOCAL_LLM_URL = (process.env.PRISM_LOCAL_LLM_URL || "http://127.0.0.1:11434").trim();
357
358
  /** Per-call timeout in ms. Prevents stalled background tasks. Capped at 300s. */
358
359
  export const PRISM_LOCAL_LLM_TIMEOUT_MS = (() => {
359
360
  const raw = parseInt(process.env.PRISM_LOCAL_LLM_TIMEOUT_MS || "60000", 10);
@@ -0,0 +1,120 @@
1
+ import { configuredOmlxModel, detectOmlx, hasOmlxControl, inspectOmlx, inspectOmlxCodingModels, OmlxError } from "../utils/omlxProvider.js";
2
+ import { localCodingPreference } from "../utils/localCodingPreference.js";
3
+ import { readLocalCodingSettings, saveLocalCodingSettings, resolveLocalCodingModel } from "../utils/localCodingSettings.js";
4
+ import { omlxSetup, OmlxSetupError, OMLX_SETUP_MODELS } from "../utils/omlxSetup.js";
5
+ import { inspectOllamaCodingModels } from "../utils/ollamaCodingModels.js";
6
+ import { getAvailableMemoryBytes } from "../utils/availableMemory.js";
7
+ const MAX_BODY_BYTES = 4096;
8
+ const defaults = { setup: omlxSetup, settings: readLocalCodingSettings, save: saveLocalCodingSettings,
9
+ preference: localCodingPreference, inspect: inspectOmlx, detect: detectOmlx, control: hasOmlxControl, configuredModel: configuredOmlxModel,
10
+ omlxModels: inspectOmlxCodingModels, ollamaModels: inspectOllamaCodingModels, availableMemory: getAvailableMemoryBytes };
11
+ function json(res, status, value) {
12
+ res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "private, no-store" });
13
+ res.end(JSON.stringify(value));
14
+ }
15
+ function loopback(req) {
16
+ return ["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(req.socket.remoteAddress ?? "");
17
+ }
18
+ async function readBody(req) {
19
+ const chunks = [];
20
+ let size = 0;
21
+ for await (const data of req) {
22
+ const chunk = Buffer.from(data);
23
+ size += chunk.length;
24
+ if (size > MAX_BODY_BYTES)
25
+ throw new Error("invalid_body");
26
+ chunks.push(chunk);
27
+ }
28
+ return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
29
+ }
30
+ /** Mounted only after the dashboard's Host, token and authentication gates. */
31
+ export async function handleLocalCodingRoutes(url, req, res, overrides = {}) {
32
+ if (url.pathname !== "/api/local-coding" && !url.pathname.startsWith("/api/local-coding/"))
33
+ return false;
34
+ const deps = { ...defaults, ...overrides };
35
+ try {
36
+ if (req.method === "GET" && url.pathname === "/api/local-coding") {
37
+ const settings = await deps.settings();
38
+ let preference, preferenceError;
39
+ try {
40
+ preference = await deps.preference();
41
+ }
42
+ catch {
43
+ preferenceError = "Backend preference inspection failed. You can change the saved settings below; check PRISM_LOCAL_CODING_BACKEND if an environment override is set.";
44
+ // Recovery controls remain available. Inference still refuses a
45
+ // broken configuration rather than silently changing backend.
46
+ preference = { saved: settings.backend, effective: settings.backend, envOverride: null, configured: false };
47
+ }
48
+ const managed = await deps.setup.status();
49
+ const inventories = await Promise.allSettled([deps.omlxModels(), deps.ollamaModels()]);
50
+ const discovered = { omlx: inventories[0].status === "fulfilled" ? inventories[0].value : [],
51
+ ollama: inventories[1].status === "fulfilled" ? inventories[1].value : [] };
52
+ const discoveryErrors = { omlx: inventories[0].status === "rejected" ? "oMLX model metadata unavailable; memory requirements unknown." : null,
53
+ ollama: inventories[1].status === "rejected" ? "Ollama model inventory unavailable. Check the configured server." : null };
54
+ let connection = { connected: false, control: false, models: [] };
55
+ try {
56
+ const observation = await deps.inspect();
57
+ connection = { connected: true, control: false, models: observation.advertised_models, adminUrl: new URL(observation.url).origin + "/admin" };
58
+ const model = resolveLocalCodingModel(settings, "omlx", "coder") ?? await deps.configuredModel();
59
+ if (model) {
60
+ const target = await deps.detect(model);
61
+ connection.model = model;
62
+ connection.control = await deps.control(target);
63
+ }
64
+ else
65
+ connection.error = "Choose an advertised coding model and save the preference.";
66
+ }
67
+ catch (error) {
68
+ connection.error = error instanceof OmlxError ? error.reason : "Connection check unavailable.";
69
+ }
70
+ json(res, 200, { settings, preference, preferenceError, managed, connection, discovered, discoveryErrors,
71
+ availableMemoryBytes: deps.availableMemory(), canManage: loopback(req),
72
+ requiredClassifier: OMLX_SETUP_MODELS[0].id });
73
+ return true;
74
+ }
75
+ if (req.method !== "POST") {
76
+ json(res, 405, { error: "Method not allowed" });
77
+ return true;
78
+ }
79
+ // Installing packages is a local operator action. A configured remote
80
+ // dashboard origin and cross-port browser requests do not authorize it.
81
+ const origin = req.headers.origin;
82
+ let sameOrigin = !origin;
83
+ try {
84
+ if (origin)
85
+ sameOrigin = new URL(origin).host === req.headers.host;
86
+ }
87
+ catch {
88
+ sameOrigin = false;
89
+ }
90
+ if (!loopback(req) || !sameOrigin || !req.headers["content-type"]?.startsWith("application/json")) {
91
+ json(res, 403, { error: "Open this dashboard on this Mac to change local coding setup." });
92
+ return true;
93
+ }
94
+ const body = await readBody(req);
95
+ if (url.pathname === "/api/local-coding/preferences") {
96
+ await deps.save(body);
97
+ json(res, 200, { saved: true });
98
+ return true;
99
+ }
100
+ if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length) {
101
+ json(res, 400, { error: "This action accepts no custom setup arguments." });
102
+ return true;
103
+ }
104
+ if (url.pathname === "/api/local-coding/install") {
105
+ json(res, 202, await deps.setup.install());
106
+ return true;
107
+ }
108
+ if (url.pathname === "/api/local-coding/start") {
109
+ json(res, 202, await deps.setup.start());
110
+ return true;
111
+ }
112
+ json(res, 404, { error: "Unknown local coding action" });
113
+ return true;
114
+ }
115
+ catch (error) {
116
+ const message = error instanceof OmlxSetupError ? error.message : "Local coding setup unavailable. Check the saved settings or local setup log and retry.";
117
+ json(res, 400, { error: message });
118
+ return true;
119
+ }
120
+ }
@@ -0,0 +1,171 @@
1
+ import { OMLX_SETUP_MODELS } from "../utils/omlxSetup.js";
2
+ export function localCodingPanelHTML() {
3
+ return `<div class="s-tab-panel" id="spanel-local">
4
+ <div class="setting-section">Local coding</div>
5
+ <p class="setting-desc">Choose where coding drafts and local reviews run. Models are discovered from your servers. The same model can serve both roles. Explicit task choices override these preferences. Tool routing and images keep their supported backend.</p>
6
+ <form id="local-coding-form" onsubmit="saveLocalCoding(event)" style="display:grid;gap:14px;margin-top:18px">
7
+ <label class="setting-label" for="local-coding-backend">Preferred coding backend</label>
8
+ <select class="setting-select" id="local-coding-backend" style="width:100%;max-width:100%" disabled>
9
+ <option value="auto">Auto — prefer configured oMLX</option><option value="omlx">oMLX</option><option value="ollama">Ollama</option>
10
+ </select>
11
+ <label class="setting-label" for="local-coding-source">oMLX connection</label>
12
+ <select class="setting-select" id="local-coding-source" style="width:100%;max-width:100%" disabled>
13
+ <option value="existing">Existing oMLX server</option><option value="managed">Prism-managed installation</option>
14
+ </select>
15
+ ${["omlx", "ollama"].map(backend => `<fieldset style="min-width:0;border:1px solid var(--border-glass);border-radius:8px;padding:12px;display:grid;gap:10px"><legend class="setting-label">${backend === "omlx" ? "oMLX" : "Ollama"} models</legend>
16
+ ${["coder", "reviewer"].map(role => `<label class="setting-label" for="local-coding-${backend}-${role}">${role === "coder" ? "Coder" : "Reviewer"}</label><select class="setting-select" id="local-coding-${backend}-${role}" style="width:100%;max-width:100%" onchange="renderLocalCodingMemory()" disabled></select><p class="setting-desc" id="local-coding-${backend}-${role}-memory" style="margin:0;overflow-wrap:anywhere"></p>`).join("")}
17
+ <p class="setting-desc" id="local-coding-${backend}-discovery" style="margin:0;overflow-wrap:anywhere" role="status"></p>
18
+ </fieldset>`).join("")}
19
+ <p class="setting-desc" id="local-coding-memory" style="margin:0"></p>
20
+ <div class="skill-actions" style="flex-wrap:wrap"><button class="skill-save-btn" id="local-coding-save" type="submit" disabled>Save coding preference</button><button class="skill-upload-btn" type="button" onclick="loadLocalCoding(false)">Check saved connection</button></div>
21
+ </form>
22
+ <p id="local-coding-saved" class="setting-desc" aria-live="polite"></p>
23
+ <div id="local-coding-connection" class="setting-desc" role="status" style="margin:16px 0;padding:12px;border:1px solid var(--border-glass);border-radius:8px">Checking local coding setup…</div>
24
+ <a id="local-coding-admin" target="_blank" rel="noopener noreferrer" style="display:none">Open oMLX settings</a>
25
+ <div class="setting-section" style="margin-top:24px">Set up oMLX on this Mac</div>
26
+ <p class="setting-desc">Use your existing server to avoid duplicate downloads. New setup installs the pinned Prism-compatible server in an isolated Python environment. Downloads are opt-in; npm installation never starts them.</p>
27
+ <p class="setting-desc">Apple Silicon · macOS 15+ · Python 3.11–3.13 · 32 GiB unified memory · 40 GiB free disk. The standard install uses generic kernels; full Xcode builds can be faster.</p>
28
+ <ul class="setting-desc" style="line-height:1.8;padding-left:20px">${OMLX_SETUP_MODELS.map(model => `<li><a href="https://huggingface.co/${model.repo}" target="_blank" rel="noopener noreferrer">${model.label}</a> — about ${model.approximateGB} GB</li>`).join("")}</ul>
29
+ <p class="setting-desc">This optional starter pack does not restrict your model choices above. Review the model licenses before downloading. Managed setup uses port 8001, keeps a 10 GB cache limit, and leaves existing oMLX and Ollama processes running. Prism's oMLX path requires a 27B model ceiling from the task and plan, plus available memory.</p>
30
+ <div id="local-coding-setup-state" role="status" class="setting-desc" style="margin:14px 0"></div>
31
+ <progress id="local-coding-progress" aria-label="oMLX setup progress" style="display:none;width:100%"></progress>
32
+ <div class="skill-actions" style="flex-wrap:wrap"><button class="skill-save-btn" type="button" id="local-coding-install" onclick="localCodingAction('install')" disabled>Install oMLX + recommended models</button><button class="skill-upload-btn" type="button" id="local-coding-start" onclick="localCodingAction('start')" disabled>Start managed oMLX</button></div>
33
+ <p class="setting-desc" style="margin-top:12px">After starting, select <strong>Prism-managed installation</strong> above and save. Managed connection settings stay separate from your existing server. Drafts require host review and tests.</p>
34
+ <p id="local-coding-error" role="alert" style="color:var(--accent-red);overflow-wrap:anywhere"></p>
35
+ </div>`;
36
+ }
37
+ /** ES5 to match the dashboard's inline-script compatibility contract. */
38
+ export function localCodingScript() {
39
+ return `
40
+ var localCodingLoaded = false;
41
+ var localCodingBusy = false;
42
+ var localCodingPoll;
43
+ var localCodingReadSequence = 0;
44
+ var localCodingStartingUntil = 0;
45
+ var localCodingActionError = '';
46
+ var localCodingModels = {omlx:[],ollama:[]};
47
+ var localCodingDiscoveryErrors = {};
48
+ function localCodingGiB(bytes) { return (bytes / Math.pow(1024, 3)).toFixed(1) + ' GiB'; }
49
+ function renderLocalCodingMemory() {
50
+ ['omlx','ollama'].forEach(function(backend) {
51
+ ['coder','reviewer'].forEach(function(role) {
52
+ var id = document.getElementById('local-coding-' + backend + '-' + role).value;
53
+ var model = localCodingModels[backend].filter(function(item) { return item.id === id; })[0];
54
+ var text = !id ? 'Uses the backend default. No role-specific model is saved.' : !model ? (localCodingDiscoveryErrors[backend] ? 'Inventory unavailable; model availability and memory are unknown.' : 'Saved model is unavailable. It will be refused rather than replaced.') :
55
+ (model.weightBytes ? 'Reported weights: ' + localCodingGiB(model.weightBytes) : 'Weight size unknown') +
56
+ (model.loadedBytes ? ' · Loaded memory: ' + localCodingGiB(model.loadedBytes) : model.loaded === true ? ' · Loaded; memory unknown' : model.loaded === false ? ' · Not loaded' : ' · Load status unknown') +
57
+ (model.weightBytes ? ' · Cold-load RAM estimate: at least ' + localCodingGiB(Math.max(model.weightBytes * 1.5, model.weightBytes + 2 * Math.pow(1024,3))) : ' · RAM requirements unknown');
58
+ document.getElementById('local-coding-' + backend + '-' + role + '-memory').textContent = text;
59
+ });
60
+ });
61
+ }
62
+ function localCodingRequest(path, body) {
63
+ return fetch('/api/local-coding' + path, body === undefined ? {cache:'no-store'} : {
64
+ method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)
65
+ }).then(function(response) { return response.json().then(function(data) {
66
+ if (!response.ok || data.error) throw new Error(data.error || 'Local coding request failed.');
67
+ return data;
68
+ }); });
69
+ }
70
+ function loadLocalCoding(reset) {
71
+ clearTimeout(localCodingPoll);
72
+ var sequence = ++localCodingReadSequence;
73
+ return localCodingRequest('').then(function(data) {
74
+ if (sequence !== localCodingReadSequence) return data;
75
+ var settings = data.settings;
76
+ if (reset || !localCodingLoaded) {
77
+ document.getElementById('local-coding-backend').value = settings.backend;
78
+ document.getElementById('local-coding-source').value = settings.source;
79
+ }
80
+ ['backend','source','save'].forEach(function(name) { document.getElementById('local-coding-' + name).disabled = !data.canManage || localCodingBusy; });
81
+ document.getElementById('local-coding-backend').disabled = data.preference.envOverride !== null || !data.canManage || localCodingBusy;
82
+ var override = data.preference.envOverride;
83
+ document.getElementById('local-coding-saved').textContent = 'Saved: ' + settings.backend + ' · ' + settings.source + (override ? ' · Environment override: ' + override : ' · Applies to new tasks without restarting');
84
+ var connection = data.connection;
85
+ var classifier = connection.models.indexOf(data.requiredClassifier) >= 0;
86
+ document.getElementById('local-coding-connection').textContent = !connection.connected ? 'oMLX not connected. Start or configure the selected server.' :
87
+ 'oMLX connected · ' + (connection.model || 'choose a coding model') + ' · ' + (connection.control ? 'Prism control available' : 'Prism control unavailable') + ' · ' + (classifier ? 'classifier advertised' : 'required classifier not advertised');
88
+ if (connection.error) document.getElementById('local-coding-connection').textContent += ' · ' + connection.error;
89
+ localCodingModels = data.discovered || {omlx:[],ollama:[]};
90
+ localCodingDiscoveryErrors = data.discoveryErrors || {};
91
+ // Older compatible servers may advertise IDs without memory metadata.
92
+ if (!localCodingModels.omlx.length && connection.connected) localCodingModels.omlx = connection.models.map(function(id) { return {id:id}; });
93
+ ['omlx','ollama'].forEach(function(backend) {
94
+ ['coder','reviewer'].forEach(function(role) {
95
+ var select = document.getElementById('local-coding-' + backend + '-' + role);
96
+ var saved = settings.models && settings.models[backend] && settings.models[backend][role];
97
+ if (!saved && backend === 'omlx' && role === 'coder') saved = settings.model;
98
+ var value = reset || !localCodingLoaded ? saved || '' : select.value;
99
+ select.textContent = '';
100
+ var automatic = document.createElement('option'); automatic.value = ''; automatic.textContent = 'Use backend default'; select.appendChild(automatic);
101
+ localCodingModels[backend].forEach(function(model) { var option = document.createElement('option'); option.value = model.id; option.textContent = model.id; select.appendChild(option); });
102
+ if (value && !localCodingModels[backend].some(function(model) { return model.id === value; })) {
103
+ var unavailable = document.createElement('option'); unavailable.value = value; unavailable.textContent = (localCodingDiscoveryErrors[backend] ? 'Availability unknown: ' : 'Unavailable: ') + value; select.appendChild(unavailable);
104
+ }
105
+ select.value = value;
106
+ select.disabled = !data.canManage || localCodingBusy;
107
+ });
108
+ document.getElementById('local-coding-' + backend + '-discovery').textContent = data.discoveryErrors && data.discoveryErrors[backend] || localCodingModels[backend].length + ' models detected. Refresh checks inventory without loading models.';
109
+ });
110
+ localCodingLoaded = true;
111
+ document.getElementById('local-coding-memory').textContent = 'Available RAM: ' + (data.availableMemoryBytes ? localCodingGiB(data.availableMemoryBytes) : 'unknown') + '. RAM estimates exclude workload-dependent peaks and do not guarantee a fit. Runtime admission checks still apply.';
112
+ renderLocalCodingMemory();
113
+ var admin = document.getElementById('local-coding-admin');
114
+ admin.style.display = connection.adminUrl ? 'inline' : 'none';
115
+ if (connection.adminUrl) admin.href = connection.adminUrl;
116
+ var state = data.managed, job = state.job, running = job && job.state === 'running';
117
+ document.getElementById('local-coding-setup-state').textContent = (state.installed ? 'Server installed' : 'Server not installed') + ' · ' + state.models.filter(function(model) { return model.downloaded; }).length + '/' + state.models.length + ' models downloaded · ' + state.freeDiskGB + ' GiB free' + (job ? ' · ' + job.stage + ' (' + job.state + ')' : '') + (state.server.connected ? ' · Managed server connected' : '');
118
+ document.getElementById('local-coding-progress').style.display = running ? 'block' : 'none';
119
+ var install = document.getElementById('local-coding-install');
120
+ install.disabled = !data.canManage || !state.supported || running || localCodingBusy;
121
+ install.textContent = job && (job.state === 'failed' || job.state === 'interrupted') ? 'Retry / resume setup' : state.installed && state.models.every(function(model) { return model.downloaded; }) ? 'Verify installed setup' : 'Install oMLX + recommended models';
122
+ document.getElementById('local-coding-start').disabled = !data.canManage || !state.installed || state.models.some(function(model) { return !model.downloaded; }) || running || localCodingBusy || state.server.connected;
123
+ if (!state.supported) document.getElementById('local-coding-setup-state').textContent += ' · Installation requires Apple Silicon with macOS 15+';
124
+ if (!data.canManage) document.getElementById('local-coding-error').textContent = 'Open the dashboard on this Mac to manage local setup.';
125
+ else document.getElementById('local-coding-error').textContent = localCodingActionError || data.preferenceError || (job && job.error) || '';
126
+ if (state.server.connected) localCodingStartingUntil = 0;
127
+ if (running || localCodingBusy || Date.now() < localCodingStartingUntil) localCodingPoll = setTimeout(function() { loadLocalCoding(false); }, 4000);
128
+ return data;
129
+ }).catch(function(error) {
130
+ if (sequence !== localCodingReadSequence) return;
131
+ document.getElementById('local-coding-error').textContent = error.message;
132
+ ['save','install','start'].forEach(function(name) { document.getElementById('local-coding-' + name).disabled = true; });
133
+ });
134
+ }
135
+ function saveLocalCoding(event) {
136
+ event.preventDefault();
137
+ var settings = {backend:document.getElementById('local-coding-backend').value,source:document.getElementById('local-coding-source').value};
138
+ settings.models = {};
139
+ ['omlx','ollama'].forEach(function(backend) {
140
+ settings.models[backend] = {};
141
+ ['coder','reviewer'].forEach(function(role) {
142
+ var model = document.getElementById('local-coding-' + backend + '-' + role).value;
143
+ if (model) settings.models[backend][role] = model;
144
+ });
145
+ });
146
+ localCodingBusy = true;
147
+ localCodingActionError = '';
148
+ localCodingReadSequence++;
149
+ document.getElementById('local-coding-error').textContent = '';
150
+ document.getElementById('local-coding-save').disabled = true;
151
+ return localCodingRequest('/preferences', settings).then(function() {
152
+ localCodingBusy = false;
153
+ return loadLocalCoding(true);
154
+ }).catch(function(error) { localCodingBusy = false; document.getElementById('local-coding-error').textContent = error.message; document.getElementById('local-coding-save').disabled = false; });
155
+ }
156
+ function localCodingAction(action) {
157
+ localCodingBusy = true;
158
+ localCodingActionError = '';
159
+ localCodingReadSequence++;
160
+ document.getElementById('local-coding-error').textContent = '';
161
+ document.getElementById('local-coding-setup-state').textContent = action === 'start' ? 'Starting managed oMLX. Waiting for the server connection…' : 'Checking prerequisites before setup…';
162
+ document.getElementById('local-coding-progress').style.display = 'block';
163
+ ['install','start'].forEach(function(name) { document.getElementById('local-coding-' + name).disabled = true; });
164
+ return localCodingRequest('/' + action, {}).then(function() {
165
+ localCodingBusy = false;
166
+ if (action === 'start') localCodingStartingUntil = Date.now() + 90000;
167
+ return loadLocalCoding(false);
168
+ }).catch(function(error) { localCodingBusy = false; localCodingActionError = error.message; document.getElementById('local-coding-error').textContent = error.message; loadLocalCoding(false); });
169
+ }
170
+ `;
171
+ }
@@ -36,6 +36,7 @@ import { buildVaultDirectory } from "../utils/vaultExporter.js";
36
36
  import { redactSettings } from "../tools/commonHelpers.js";
37
37
  import { handleGraphRoutes } from "./graphRouter.js";
38
38
  import { handleAccountRoutes } from "./accountRouter.js";
39
+ import { handleLocalCodingRoutes } from "./localCodingRouter.js";
39
40
  import { createDashboardStorageAccessor } from "./storageAccessor.js";
40
41
  import { DASHBOARD_READ_MESSAGES } from "./readMessages.js";
41
42
  import { isDashboardSettingKeyAllowed, isDashboardSettingValueAllowed } from "./settingsPolicy.js";
@@ -697,6 +698,8 @@ return false;}
697
698
  return res.end(JSON.stringify({ ok: true, role, namespace: "user_skill" }));
698
699
  }
699
700
  // ─── API: Synalux Account & Subscription ───
701
+ if (await handleLocalCodingRoutes(url, req, res))
702
+ return;
700
703
  if (url.pathname.startsWith("/api/account")) {
701
704
  if (await handleAccountRoutes(url, req, res))
702
705
  return;