prism-mcp-server 20.22.3 → 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 +79 -70
- package/dist/cli.js +42 -0
- package/dist/dashboard/localCodingRouter.js +120 -0
- package/dist/dashboard/localCodingUI.js +171 -0
- package/dist/dashboard/server.js +3 -0
- package/dist/dashboard/ui.js +10 -1
- package/dist/storage/configStorage.js +2 -2
- package/dist/tools/prismInferHandler.js +115 -48
- package/dist/utils/localCodingPreference.js +24 -0
- package/dist/utils/localCodingSettings.js +55 -0
- package/dist/utils/ollamaCodingModels.js +93 -0
- package/dist/utils/omlxProvider.js +78 -13
- package/dist/utils/omlxSetup.js +432 -0
- package/dist/utils/omlxSetupWorker.js +23 -0
- package/package.json +1 -1
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
|
[](https://www.npmjs.com/package/prism-mcp-server)
|
|
6
11
|
[](https://github.com/modelcontextprotocol/servers)
|
|
7
12
|
[](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** —
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
###
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
This path
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
**Requires** the [stable Prism integration of oMLX](https://github.com/dcostenco/omlx),
|
|
74
|
-
based on upstream `v0.7.1` with Prism's control routes
|
|
75
|
-
([install and settings](https://github.com/dcostenco/omlx/blob/main/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,26 @@ 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
|
+
|
|
238
211
|
## What's New in v20.22.3
|
|
239
212
|
|
|
240
213
|
- **Dependency fixes:** Hono 4.13.11 addresses its static-path middleware
|
|
@@ -1348,6 +1321,32 @@ requests are refused rather than answered blind when no tier (or the Layer 1
|
|
|
1348
1321
|
safety classifier) can actually see the image, so a text-only model is never
|
|
1349
1322
|
handed a prompt about a screenshot it never received. The 27b is text only.
|
|
1350
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
|
+
|
|
1351
1350
|
---
|
|
1352
1351
|
|
|
1353
1352
|
## What it does
|
|
@@ -1498,6 +1497,9 @@ and `verify` govern the other two channels.
|
|
|
1498
1497
|
|
|
1499
1498
|
## Models
|
|
1500
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
|
+
|
|
1501
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.
|
|
1502
1504
|
|
|
1503
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.
|
|
@@ -1568,7 +1570,7 @@ selection.
|
|
|
1568
1570
|
|
|
1569
1571
|
| Layer | What | Model | Cost |
|
|
1570
1572
|
|---|---|---|---|
|
|
1571
|
-
| **L1** | Crisis/medical safety
|
|
1573
|
+
| **L1** | Crisis/medical safety rules and local classification | Backend-specific classifier (required 9B on oMLX) | Varies |
|
|
1572
1574
|
| **L3-Registry** | Envelope validation + advertised-tool enforcement (all tiers) | None | 0 ms |
|
|
1573
1575
|
| **L3-Route** | Authenticated deterministic route correction (Standard+) | None | Network latency |
|
|
1574
1576
|
| **L3-Tier0** | Integer grounding (set membership) | None (deterministic) | 0 ms |
|
|
@@ -1613,10 +1615,14 @@ check, the local 9B passed 2/3 tasks; the local 27B and Gemini 3.6 Flash each
|
|
|
1613
1615
|
passed 3/3. This is a self-published regression signal, not an independent
|
|
1614
1616
|
leaderboard or a claim of broad model equivalence.
|
|
1615
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
|
+
|
|
1616
1621
|
### Cloud Escalation (`cloud_fallback`)
|
|
1617
1622
|
|
|
1618
|
-
Prism
|
|
1619
|
-
an empty, truncated, think-only, or looping
|
|
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
|
|
1620
1626
|
finds the request uncertain or reserved, a paid plan escalates the request
|
|
1621
1627
|
through Gemini 3.6 Flash. Free-tier routing stays local and reports the
|
|
1622
1628
|
outcome without making a cloud call.
|
|
@@ -1627,6 +1633,9 @@ for a call, which the clinical delegation rules do for drafting; pass `true`
|
|
|
1627
1633
|
to ask for it, which still requires a plan with cloud. A request that carries
|
|
1628
1634
|
an image is never escalated; screenshots stay on this device.
|
|
1629
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
|
+
|
|
1630
1639
|
---
|
|
1631
1640
|
|
|
1632
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);
|
|
@@ -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
|
+
}
|
package/dist/dashboard/server.js
CHANGED
|
@@ -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;
|