synthux-mcp 0.0.0-stage → 2.1.1
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 +84 -2
- package/apple-fm-bridge.swift +277 -0
- package/dist/apple-fm-bridge.swift +277 -0
- package/dist/cli.js +3674 -0
- package/package.json +45 -4
- package/rules/ecommerce.json +163 -0
- package/rules/fintech.json +163 -0
- package/rules/flow-heuristics.json +58 -0
- package/rules/nielsen-10.json +201 -0
- package/rules/saas.json +163 -0
- package/server.js +1794 -0
package/README.md
CHANGED
|
@@ -1,3 +1,85 @@
|
|
|
1
|
-
#
|
|
1
|
+
# synthux-mcp — Official Model Context Protocol (MCP) Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Connect desktop AI coding agents (**Google Antigravity**, **Claude Desktop / Claude Code**, **Cursor**, **OpenAI Codex**, **Windsurf**, or **Local LLM MCP Hosts**) directly to the **synthux** Chrome Extension and UX Audit Engine.
|
|
4
|
+
|
|
5
|
+
## Architecture & Security
|
|
6
|
+
|
|
7
|
+
- **Dual MCP Transport:** Supports both **Stdio** (`npx -y synthux-mcp` / `node mcp/server.js`) and **Streamable HTTP** (`http://127.0.0.1:17483/mcp`).
|
|
8
|
+
- **Live Chrome Extension Bridge:** Connects to the synthux MV3 Chrome Extension over local WebSocket (`ws://127.0.0.1:17483/ws`).
|
|
9
|
+
- **Two AI Evaluation Modes:**
|
|
10
|
+
1. **Agent-Brain Mode (Passthrough):** Your connected MCP Agent (e.g., Antigravity Gemini 4 Argon, Claude, Codex) uses `synthux_prepare_external_audit` to scan the live DOM, run `axe-core` WCAG 2.2 AA checks, and get Sectoral Rule Pack prompts — evaluates them with its own intelligence — and calls `synthux_submit_external_audit` to render live page heatmaps, annotated screenshots, and Enhanced v2.0 Markdown/JSON reports.
|
|
11
|
+
2. **Extension AI Engine Mode:** Trigger `synthux_run_ux_audit` to run the audit using the extension's configured **Local LLM (Ollama: `gemma4`, `qwen3.8`, `deepseek-r1`)**, ** Apple Intelligence (macOS 26/27 `FoundationModels`)**, **Chrome Built-in AI (`window.LanguageModel`)**, or **Cloud BYOK API (`Gemini`, `OpenAI`, `Claude`)**.
|
|
12
|
+
- **Native macOS 26/27 Apple Intelligence Bridge (`mcp/apple-fm-bridge.swift`):**
|
|
13
|
+
- Automatically compiles and caches a native Swift bridge to Apple's `FoundationModels.framework` (`SystemLanguageModel.default` & `PrivateCloudComputeLanguageModel` on macOS 27+).
|
|
14
|
+
- Exposes local HTTP endpoints on `http://127.0.0.1:17483`:
|
|
15
|
+
- `GET /apple-fm/status` & `POST /apple-fm/generate` (Native synthux Apple Intelligence provider)
|
|
16
|
+
- `GET /api/tags`, `POST /api/show`, `POST /api/generate` (Ollama-compatible endpoints)
|
|
17
|
+
- `GET /v1/models`, `POST /v1/chat/completions` (OpenAI-compatible endpoints)
|
|
18
|
+
- **Privacy & Security Hardened:**
|
|
19
|
+
- Strictly bound to `127.0.0.1` loopback (never exposed to LAN/`0.0.0.0`).
|
|
20
|
+
- Strict Origin Verification blocks all `http://` and `https://` web page origins from accessing `/ws` or `/mcp` (CSRF / WebSocket hijacking protection).
|
|
21
|
+
- Optional `SYNTHUX_MCP_TOKEN` pairing token for high-security workstations.
|
|
22
|
+
- Zero telemetry and automatic API key redaction.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quick Setup (`mcp_config.json`)
|
|
27
|
+
|
|
28
|
+
### Google Antigravity (`~/.gemini/config/mcp_config.json`) / Claude / Cursor
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"synthux": {
|
|
34
|
+
"command": "node",
|
|
35
|
+
"args": ["/path/to/synthux/mcp/server.js"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or via `npx`:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"synthux": {
|
|
47
|
+
"command": "npx",
|
|
48
|
+
"args": ["-y", "synthux-mcp"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or via **Streamable HTTP**:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"synthux": {
|
|
60
|
+
"url": "http://127.0.0.1:17483/mcp"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Available MCP Tools (14 Tools)
|
|
69
|
+
|
|
70
|
+
| Tool | Description |
|
|
71
|
+
| :--- | :--- |
|
|
72
|
+
| `synthux_status` | Check MCP bridge, Chrome extension connection, active tab, Rule Packs, Personas, and AI status |
|
|
73
|
+
| `synthux_open_url` | Open or navigate a Chrome tab to a target URL and wait for load completion |
|
|
74
|
+
| `synthux_list_tabs` | List open web tabs in Chrome |
|
|
75
|
+
| `synthux_scan_page` | Extract DOM structure, headings, landmarks, forms, navigation, CTAs, contrast, and performance |
|
|
76
|
+
| `synthux_run_wcag_audit` | Run deterministic DOM accessibility checks + live `axe-core` WCAG 2.2 AA audit |
|
|
77
|
+
| `synthux_run_ux_audit` | Run full autonomous UX audit via Local Ollama or BYOK Cloud API |
|
|
78
|
+
| `synthux_prepare_external_audit` | Prepare DOM + WCAG + Screenshot + Sectoral Rule Pack prompts for external MCP Agent evaluation |
|
|
79
|
+
| `synthux_submit_external_audit` | Submit external MCP Agent evaluations to render live Heatmap, Annotated Screenshot, and v2.0 Report |
|
|
80
|
+
| `synthux_run_flow_audit` | Run multi-page Flow Builder user journey evaluation across 2+ URLs |
|
|
81
|
+
| `synthux_visual_overlay` | Trigger live `highlight`, `heatmap`, `clear_all`, `annotated_screenshot`, or `full_screenshot` |
|
|
82
|
+
| `synthux_manage_rule_packs` | List, inspect, import/validate custom JSON rule packs, or delete custom rule packs |
|
|
83
|
+
| `synthux_manage_personas` | List, create custom synthetic user personas, or delete custom personas |
|
|
84
|
+
| `synthux_configure_ai` | Configure Local Ollama or Cloud AI provider settings and test connectivity |
|
|
85
|
+
| `synthux_get_reports` | List report history or export reports in Enhanced Markdown v2.0 or JSON format |
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env swift
|
|
2
|
+
/**
|
|
3
|
+
* synthux — Apple Foundation Models (macOS 26 / macOS 27+) Local Bridge
|
|
4
|
+
*
|
|
5
|
+
* Interfaces directly with Apple's native `FoundationModels` framework:
|
|
6
|
+
* - `SystemLanguageModel.default` (On-Device Apple Silicon Neural Engine model)
|
|
7
|
+
* - `SystemLanguageModel(useCase: .contentTagging)` (Fast structured tagging)
|
|
8
|
+
* - `PrivateCloudComputeLanguageModel()` (macOS 27+ Apple Private Cloud Compute)
|
|
9
|
+
*
|
|
10
|
+
* Commands:
|
|
11
|
+
* 1. `synthux-apple-fm status`
|
|
12
|
+
* Outputs JSON with availability, macOS version, context window size, and models.
|
|
13
|
+
* 2. `synthux-apple-fm generate`
|
|
14
|
+
* Reads JSON payload from stdin:
|
|
15
|
+
* { "model": "apple-on-device", "prompt": "...", "system": "...", "temperature": 0.3, "maxTokens": 2048 }
|
|
16
|
+
* Outputs JSON response to stdout:
|
|
17
|
+
* { "success": true, "model": "apple-on-device", "response": "{...}", "inputTokens": 0, "outputTokens": 0 }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import Foundation
|
|
21
|
+
#if canImport(FoundationModels)
|
|
22
|
+
import FoundationModels
|
|
23
|
+
#endif
|
|
24
|
+
|
|
25
|
+
struct BridgeRequest: Decodable {
|
|
26
|
+
let model: String?
|
|
27
|
+
let prompt: String
|
|
28
|
+
let system: String?
|
|
29
|
+
let temperature: Double?
|
|
30
|
+
let maxTokens: Int?
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func jsonOut(_ dict: [String: Any]) {
|
|
34
|
+
if let data = try? JSONSerialization.data(withJSONObject: dict, options: []),
|
|
35
|
+
let str = String(data: data, encoding: .utf8) {
|
|
36
|
+
FileHandle.standardOutput.write((str + "\n").data(using: .utf8)!)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func cleanJsonResponse(_ text: String) -> String {
|
|
41
|
+
var trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
42
|
+
if trimmed.hasPrefix("```json") {
|
|
43
|
+
trimmed = String(trimmed.dropFirst(7))
|
|
44
|
+
} else if trimmed.hasPrefix("```") {
|
|
45
|
+
trimmed = String(trimmed.dropFirst(3))
|
|
46
|
+
}
|
|
47
|
+
if trimmed.hasSuffix("```") {
|
|
48
|
+
trimmed = String(trimmed.dropLast(3))
|
|
49
|
+
}
|
|
50
|
+
return trimmed.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func osVersionString() -> String {
|
|
54
|
+
let v = ProcessInfo.processInfo.operatingSystemVersion
|
|
55
|
+
return "macOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
struct SynthuxAppleFMBridge {
|
|
59
|
+
static func run() async {
|
|
60
|
+
let args = CommandLine.arguments
|
|
61
|
+
let command = args.count > 1 ? args[1] : "status"
|
|
62
|
+
|
|
63
|
+
#if canImport(FoundationModels)
|
|
64
|
+
if #available(macOS 26.0, *) {
|
|
65
|
+
if command == "status" {
|
|
66
|
+
await handleStatus()
|
|
67
|
+
return
|
|
68
|
+
} else if command == "generate" {
|
|
69
|
+
await handleGenerate()
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
#endif
|
|
74
|
+
|
|
75
|
+
jsonOut([
|
|
76
|
+
"available": false,
|
|
77
|
+
"status": "unsupported_os",
|
|
78
|
+
"reason": "Apple FoundationModels framework requires macOS 26.0 or newer (macOS 27 recommended).",
|
|
79
|
+
"osVersion": osVersionString(),
|
|
80
|
+
"models": []
|
|
81
|
+
])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#if canImport(FoundationModels)
|
|
85
|
+
@available(macOS 26.0, *)
|
|
86
|
+
static func handleStatus() async {
|
|
87
|
+
let defaultModel = SystemLanguageModel.default
|
|
88
|
+
var statusStr = "connected"
|
|
89
|
+
var reasonStr: String? = nil
|
|
90
|
+
var isAvailable = false
|
|
91
|
+
|
|
92
|
+
switch defaultModel.availability {
|
|
93
|
+
case .available:
|
|
94
|
+
isAvailable = true
|
|
95
|
+
statusStr = "connected"
|
|
96
|
+
case .unavailable(let reason):
|
|
97
|
+
isAvailable = false
|
|
98
|
+
switch reason {
|
|
99
|
+
case .deviceNotEligible:
|
|
100
|
+
statusStr = "device_not_eligible"
|
|
101
|
+
reasonStr = "This Mac is not eligible for Apple Intelligence (Apple Silicon M1 or newer required)."
|
|
102
|
+
case .appleIntelligenceNotEnabled:
|
|
103
|
+
statusStr = "apple_intelligence_disabled"
|
|
104
|
+
reasonStr = "Apple Intelligence is disabled. Enable it in System Settings → Apple Intelligence & Siri."
|
|
105
|
+
case .modelNotReady:
|
|
106
|
+
statusStr = "model_not_ready"
|
|
107
|
+
reasonStr = "Apple Foundation Model is still preparing or downloading in macOS System Settings."
|
|
108
|
+
@unknown default:
|
|
109
|
+
statusStr = "unavailable"
|
|
110
|
+
reasonStr = "Apple Foundation Model is currently unavailable."
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var contextSize = 4096
|
|
115
|
+
var variantName = "Apple On-Device Foundation Model"
|
|
116
|
+
var pccAvailable = false
|
|
117
|
+
|
|
118
|
+
if #available(macOS 27.0, *) {
|
|
119
|
+
if defaultModel.contextSize > 0 {
|
|
120
|
+
contextSize = defaultModel.contextSize
|
|
121
|
+
}
|
|
122
|
+
variantName = defaultModel.variant.displayName
|
|
123
|
+
let pccModel = PrivateCloudComputeLanguageModel()
|
|
124
|
+
pccAvailable = pccModel.isAvailable
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
var models: [[String: Any]] = [
|
|
128
|
+
[
|
|
129
|
+
"id": "apple-on-device",
|
|
130
|
+
"name": "Apple Foundation Model (On-Device ANE)",
|
|
131
|
+
"size": 0
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
"id": "apple-content-tagging",
|
|
135
|
+
"name": "Apple Foundation Model (Fast Tagging)",
|
|
136
|
+
"size": 0
|
|
137
|
+
]
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
if #available(macOS 27.0, *) {
|
|
141
|
+
models.insert([
|
|
142
|
+
"id": "apple-pcc",
|
|
143
|
+
"name": "Apple Private Cloud Compute (macOS 27)",
|
|
144
|
+
"size": 0
|
|
145
|
+
], at: 1)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
jsonOut([
|
|
149
|
+
"available": isAvailable,
|
|
150
|
+
"status": statusStr,
|
|
151
|
+
"reason": reasonStr as Any,
|
|
152
|
+
"osVersion": osVersionString(),
|
|
153
|
+
"contextSize": contextSize,
|
|
154
|
+
"variant": variantName,
|
|
155
|
+
"pccAvailable": pccAvailable,
|
|
156
|
+
"models": models
|
|
157
|
+
])
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@available(macOS 26.0, *)
|
|
161
|
+
static func handleGenerate() async {
|
|
162
|
+
let inputData = FileHandle.standardInput.readDataToEndOfFile()
|
|
163
|
+
guard !inputData.isEmpty,
|
|
164
|
+
let req = try? JSONDecoder().decode(BridgeRequest.self, from: inputData) else {
|
|
165
|
+
jsonOut([
|
|
166
|
+
"success": false,
|
|
167
|
+
"error": "Invalid JSON input payload for Apple Foundation Model bridge."
|
|
168
|
+
])
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let startTime = Date()
|
|
173
|
+
let requestedModel = req.model ?? "apple-on-device"
|
|
174
|
+
let temperature = req.temperature ?? 0.3
|
|
175
|
+
let maxTokens = req.maxTokens ?? 2048
|
|
176
|
+
let options = GenerationOptions(
|
|
177
|
+
temperature: temperature,
|
|
178
|
+
maximumResponseTokens: maxTokens
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Trim prompt safely if it exceeds on-device context budget (~12,000 chars ≈ 3,000 tokens)
|
|
182
|
+
let maxPromptChars = requestedModel == "apple-pcc" ? 28000 : 12000
|
|
183
|
+
let safePrompt: String
|
|
184
|
+
if req.prompt.count > maxPromptChars {
|
|
185
|
+
safePrompt = String(req.prompt.prefix(maxPromptChars)) + "\n\n[DOM truncated to fit on-device context window. Respond ONLY with valid JSON.]"
|
|
186
|
+
} else {
|
|
187
|
+
safePrompt = req.prompt
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
do {
|
|
191
|
+
var responseText = ""
|
|
192
|
+
var inputTokens = max(1, safePrompt.count / 4)
|
|
193
|
+
var outputTokens = 0
|
|
194
|
+
|
|
195
|
+
if #available(macOS 27.0, *), requestedModel == "apple-pcc" {
|
|
196
|
+
let pccModel = PrivateCloudComputeLanguageModel()
|
|
197
|
+
if pccModel.isAvailable {
|
|
198
|
+
let session = LanguageModelSession(
|
|
199
|
+
model: pccModel,
|
|
200
|
+
instructions: req.system
|
|
201
|
+
)
|
|
202
|
+
let res = try await session.respond(to: safePrompt, options: options)
|
|
203
|
+
responseText = cleanJsonResponse(res.content)
|
|
204
|
+
inputTokens = res.usage.input.totalTokenCount
|
|
205
|
+
outputTokens = res.usage.output.totalTokenCount
|
|
206
|
+
} else {
|
|
207
|
+
// Fallback to on-device model if PCC is not ready
|
|
208
|
+
let onDevice = SystemLanguageModel(
|
|
209
|
+
useCase: .general,
|
|
210
|
+
guardrails: .permissiveContentTransformations
|
|
211
|
+
)
|
|
212
|
+
let session = LanguageModelSession(
|
|
213
|
+
model: onDevice,
|
|
214
|
+
instructions: req.system
|
|
215
|
+
)
|
|
216
|
+
let res = try await session.respond(to: safePrompt, options: options)
|
|
217
|
+
responseText = cleanJsonResponse(res.content)
|
|
218
|
+
inputTokens = res.usage.input.totalTokenCount
|
|
219
|
+
outputTokens = res.usage.output.totalTokenCount
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
let useCase: SystemLanguageModel.UseCase = (requestedModel == "apple-content-tagging")
|
|
223
|
+
? .contentTagging
|
|
224
|
+
: .general
|
|
225
|
+
let onDevice = SystemLanguageModel(
|
|
226
|
+
useCase: useCase,
|
|
227
|
+
guardrails: .permissiveContentTransformations
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
guard onDevice.isAvailable else {
|
|
231
|
+
jsonOut([
|
|
232
|
+
"success": false,
|
|
233
|
+
"error": "Apple Foundation Model is not available on this Mac (\(onDevice.availability)). Make sure Apple Intelligence is enabled in System Settings."
|
|
234
|
+
])
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let session = LanguageModelSession(
|
|
239
|
+
model: onDevice,
|
|
240
|
+
instructions: req.system
|
|
241
|
+
)
|
|
242
|
+
let res = try await session.respond(to: safePrompt, options: options)
|
|
243
|
+
responseText = cleanJsonResponse(res.content)
|
|
244
|
+
if #available(macOS 27.0, *) {
|
|
245
|
+
inputTokens = res.usage.input.totalTokenCount
|
|
246
|
+
outputTokens = res.usage.output.totalTokenCount
|
|
247
|
+
} else {
|
|
248
|
+
outputTokens = max(1, responseText.count / 4)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let elapsedNs = Int64(Date().timeIntervalSince(startTime) * 1_000_000_000)
|
|
253
|
+
|
|
254
|
+
jsonOut([
|
|
255
|
+
"success": true,
|
|
256
|
+
"model": requestedModel,
|
|
257
|
+
"response": responseText,
|
|
258
|
+
"inputTokens": inputTokens,
|
|
259
|
+
"outputTokens": outputTokens,
|
|
260
|
+
"totalDuration": elapsedNs
|
|
261
|
+
])
|
|
262
|
+
} catch {
|
|
263
|
+
jsonOut([
|
|
264
|
+
"success": false,
|
|
265
|
+
"error": "Apple Foundation Model error: \(error.localizedDescription)"
|
|
266
|
+
])
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
#endif
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
Task {
|
|
273
|
+
await SynthuxAppleFMBridge.run()
|
|
274
|
+
exit(0)
|
|
275
|
+
}
|
|
276
|
+
RunLoop.main.run()
|
|
277
|
+
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env swift
|
|
2
|
+
/**
|
|
3
|
+
* synthux — Apple Foundation Models (macOS 26 / macOS 27+) Local Bridge
|
|
4
|
+
*
|
|
5
|
+
* Interfaces directly with Apple's native `FoundationModels` framework:
|
|
6
|
+
* - `SystemLanguageModel.default` (On-Device Apple Silicon Neural Engine model)
|
|
7
|
+
* - `SystemLanguageModel(useCase: .contentTagging)` (Fast structured tagging)
|
|
8
|
+
* - `PrivateCloudComputeLanguageModel()` (macOS 27+ Apple Private Cloud Compute)
|
|
9
|
+
*
|
|
10
|
+
* Commands:
|
|
11
|
+
* 1. `synthux-apple-fm status`
|
|
12
|
+
* Outputs JSON with availability, macOS version, context window size, and models.
|
|
13
|
+
* 2. `synthux-apple-fm generate`
|
|
14
|
+
* Reads JSON payload from stdin:
|
|
15
|
+
* { "model": "apple-on-device", "prompt": "...", "system": "...", "temperature": 0.3, "maxTokens": 2048 }
|
|
16
|
+
* Outputs JSON response to stdout:
|
|
17
|
+
* { "success": true, "model": "apple-on-device", "response": "{...}", "inputTokens": 0, "outputTokens": 0 }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import Foundation
|
|
21
|
+
#if canImport(FoundationModels)
|
|
22
|
+
import FoundationModels
|
|
23
|
+
#endif
|
|
24
|
+
|
|
25
|
+
struct BridgeRequest: Decodable {
|
|
26
|
+
let model: String?
|
|
27
|
+
let prompt: String
|
|
28
|
+
let system: String?
|
|
29
|
+
let temperature: Double?
|
|
30
|
+
let maxTokens: Int?
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func jsonOut(_ dict: [String: Any]) {
|
|
34
|
+
if let data = try? JSONSerialization.data(withJSONObject: dict, options: []),
|
|
35
|
+
let str = String(data: data, encoding: .utf8) {
|
|
36
|
+
FileHandle.standardOutput.write((str + "\n").data(using: .utf8)!)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func cleanJsonResponse(_ text: String) -> String {
|
|
41
|
+
var trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
42
|
+
if trimmed.hasPrefix("```json") {
|
|
43
|
+
trimmed = String(trimmed.dropFirst(7))
|
|
44
|
+
} else if trimmed.hasPrefix("```") {
|
|
45
|
+
trimmed = String(trimmed.dropFirst(3))
|
|
46
|
+
}
|
|
47
|
+
if trimmed.hasSuffix("```") {
|
|
48
|
+
trimmed = String(trimmed.dropLast(3))
|
|
49
|
+
}
|
|
50
|
+
return trimmed.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func osVersionString() -> String {
|
|
54
|
+
let v = ProcessInfo.processInfo.operatingSystemVersion
|
|
55
|
+
return "macOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
struct SynthuxAppleFMBridge {
|
|
59
|
+
static func run() async {
|
|
60
|
+
let args = CommandLine.arguments
|
|
61
|
+
let command = args.count > 1 ? args[1] : "status"
|
|
62
|
+
|
|
63
|
+
#if canImport(FoundationModels)
|
|
64
|
+
if #available(macOS 26.0, *) {
|
|
65
|
+
if command == "status" {
|
|
66
|
+
await handleStatus()
|
|
67
|
+
return
|
|
68
|
+
} else if command == "generate" {
|
|
69
|
+
await handleGenerate()
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
#endif
|
|
74
|
+
|
|
75
|
+
jsonOut([
|
|
76
|
+
"available": false,
|
|
77
|
+
"status": "unsupported_os",
|
|
78
|
+
"reason": "Apple FoundationModels framework requires macOS 26.0 or newer (macOS 27 recommended).",
|
|
79
|
+
"osVersion": osVersionString(),
|
|
80
|
+
"models": []
|
|
81
|
+
])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#if canImport(FoundationModels)
|
|
85
|
+
@available(macOS 26.0, *)
|
|
86
|
+
static func handleStatus() async {
|
|
87
|
+
let defaultModel = SystemLanguageModel.default
|
|
88
|
+
var statusStr = "connected"
|
|
89
|
+
var reasonStr: String? = nil
|
|
90
|
+
var isAvailable = false
|
|
91
|
+
|
|
92
|
+
switch defaultModel.availability {
|
|
93
|
+
case .available:
|
|
94
|
+
isAvailable = true
|
|
95
|
+
statusStr = "connected"
|
|
96
|
+
case .unavailable(let reason):
|
|
97
|
+
isAvailable = false
|
|
98
|
+
switch reason {
|
|
99
|
+
case .deviceNotEligible:
|
|
100
|
+
statusStr = "device_not_eligible"
|
|
101
|
+
reasonStr = "This Mac is not eligible for Apple Intelligence (Apple Silicon M1 or newer required)."
|
|
102
|
+
case .appleIntelligenceNotEnabled:
|
|
103
|
+
statusStr = "apple_intelligence_disabled"
|
|
104
|
+
reasonStr = "Apple Intelligence is disabled. Enable it in System Settings → Apple Intelligence & Siri."
|
|
105
|
+
case .modelNotReady:
|
|
106
|
+
statusStr = "model_not_ready"
|
|
107
|
+
reasonStr = "Apple Foundation Model is still preparing or downloading in macOS System Settings."
|
|
108
|
+
@unknown default:
|
|
109
|
+
statusStr = "unavailable"
|
|
110
|
+
reasonStr = "Apple Foundation Model is currently unavailable."
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var contextSize = 4096
|
|
115
|
+
var variantName = "Apple On-Device Foundation Model"
|
|
116
|
+
var pccAvailable = false
|
|
117
|
+
|
|
118
|
+
if #available(macOS 27.0, *) {
|
|
119
|
+
if defaultModel.contextSize > 0 {
|
|
120
|
+
contextSize = defaultModel.contextSize
|
|
121
|
+
}
|
|
122
|
+
variantName = defaultModel.variant.displayName
|
|
123
|
+
let pccModel = PrivateCloudComputeLanguageModel()
|
|
124
|
+
pccAvailable = pccModel.isAvailable
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
var models: [[String: Any]] = [
|
|
128
|
+
[
|
|
129
|
+
"id": "apple-on-device",
|
|
130
|
+
"name": "Apple Foundation Model (On-Device ANE)",
|
|
131
|
+
"size": 0
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
"id": "apple-content-tagging",
|
|
135
|
+
"name": "Apple Foundation Model (Fast Tagging)",
|
|
136
|
+
"size": 0
|
|
137
|
+
]
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
if #available(macOS 27.0, *) {
|
|
141
|
+
models.insert([
|
|
142
|
+
"id": "apple-pcc",
|
|
143
|
+
"name": "Apple Private Cloud Compute (macOS 27)",
|
|
144
|
+
"size": 0
|
|
145
|
+
], at: 1)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
jsonOut([
|
|
149
|
+
"available": isAvailable,
|
|
150
|
+
"status": statusStr,
|
|
151
|
+
"reason": reasonStr as Any,
|
|
152
|
+
"osVersion": osVersionString(),
|
|
153
|
+
"contextSize": contextSize,
|
|
154
|
+
"variant": variantName,
|
|
155
|
+
"pccAvailable": pccAvailable,
|
|
156
|
+
"models": models
|
|
157
|
+
])
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@available(macOS 26.0, *)
|
|
161
|
+
static func handleGenerate() async {
|
|
162
|
+
let inputData = FileHandle.standardInput.readDataToEndOfFile()
|
|
163
|
+
guard !inputData.isEmpty,
|
|
164
|
+
let req = try? JSONDecoder().decode(BridgeRequest.self, from: inputData) else {
|
|
165
|
+
jsonOut([
|
|
166
|
+
"success": false,
|
|
167
|
+
"error": "Invalid JSON input payload for Apple Foundation Model bridge."
|
|
168
|
+
])
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let startTime = Date()
|
|
173
|
+
let requestedModel = req.model ?? "apple-on-device"
|
|
174
|
+
let temperature = req.temperature ?? 0.3
|
|
175
|
+
let maxTokens = req.maxTokens ?? 2048
|
|
176
|
+
let options = GenerationOptions(
|
|
177
|
+
temperature: temperature,
|
|
178
|
+
maximumResponseTokens: maxTokens
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Trim prompt safely if it exceeds on-device context budget (~12,000 chars ≈ 3,000 tokens)
|
|
182
|
+
let maxPromptChars = requestedModel == "apple-pcc" ? 28000 : 12000
|
|
183
|
+
let safePrompt: String
|
|
184
|
+
if req.prompt.count > maxPromptChars {
|
|
185
|
+
safePrompt = String(req.prompt.prefix(maxPromptChars)) + "\n\n[DOM truncated to fit on-device context window. Respond ONLY with valid JSON.]"
|
|
186
|
+
} else {
|
|
187
|
+
safePrompt = req.prompt
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
do {
|
|
191
|
+
var responseText = ""
|
|
192
|
+
var inputTokens = max(1, safePrompt.count / 4)
|
|
193
|
+
var outputTokens = 0
|
|
194
|
+
|
|
195
|
+
if #available(macOS 27.0, *), requestedModel == "apple-pcc" {
|
|
196
|
+
let pccModel = PrivateCloudComputeLanguageModel()
|
|
197
|
+
if pccModel.isAvailable {
|
|
198
|
+
let session = LanguageModelSession(
|
|
199
|
+
model: pccModel,
|
|
200
|
+
instructions: req.system
|
|
201
|
+
)
|
|
202
|
+
let res = try await session.respond(to: safePrompt, options: options)
|
|
203
|
+
responseText = cleanJsonResponse(res.content)
|
|
204
|
+
inputTokens = res.usage.input.totalTokenCount
|
|
205
|
+
outputTokens = res.usage.output.totalTokenCount
|
|
206
|
+
} else {
|
|
207
|
+
// Fallback to on-device model if PCC is not ready
|
|
208
|
+
let onDevice = SystemLanguageModel(
|
|
209
|
+
useCase: .general,
|
|
210
|
+
guardrails: .permissiveContentTransformations
|
|
211
|
+
)
|
|
212
|
+
let session = LanguageModelSession(
|
|
213
|
+
model: onDevice,
|
|
214
|
+
instructions: req.system
|
|
215
|
+
)
|
|
216
|
+
let res = try await session.respond(to: safePrompt, options: options)
|
|
217
|
+
responseText = cleanJsonResponse(res.content)
|
|
218
|
+
inputTokens = res.usage.input.totalTokenCount
|
|
219
|
+
outputTokens = res.usage.output.totalTokenCount
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
let useCase: SystemLanguageModel.UseCase = (requestedModel == "apple-content-tagging")
|
|
223
|
+
? .contentTagging
|
|
224
|
+
: .general
|
|
225
|
+
let onDevice = SystemLanguageModel(
|
|
226
|
+
useCase: useCase,
|
|
227
|
+
guardrails: .permissiveContentTransformations
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
guard onDevice.isAvailable else {
|
|
231
|
+
jsonOut([
|
|
232
|
+
"success": false,
|
|
233
|
+
"error": "Apple Foundation Model is not available on this Mac (\(onDevice.availability)). Make sure Apple Intelligence is enabled in System Settings."
|
|
234
|
+
])
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let session = LanguageModelSession(
|
|
239
|
+
model: onDevice,
|
|
240
|
+
instructions: req.system
|
|
241
|
+
)
|
|
242
|
+
let res = try await session.respond(to: safePrompt, options: options)
|
|
243
|
+
responseText = cleanJsonResponse(res.content)
|
|
244
|
+
if #available(macOS 27.0, *) {
|
|
245
|
+
inputTokens = res.usage.input.totalTokenCount
|
|
246
|
+
outputTokens = res.usage.output.totalTokenCount
|
|
247
|
+
} else {
|
|
248
|
+
outputTokens = max(1, responseText.count / 4)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let elapsedNs = Int64(Date().timeIntervalSince(startTime) * 1_000_000_000)
|
|
253
|
+
|
|
254
|
+
jsonOut([
|
|
255
|
+
"success": true,
|
|
256
|
+
"model": requestedModel,
|
|
257
|
+
"response": responseText,
|
|
258
|
+
"inputTokens": inputTokens,
|
|
259
|
+
"outputTokens": outputTokens,
|
|
260
|
+
"totalDuration": elapsedNs
|
|
261
|
+
])
|
|
262
|
+
} catch {
|
|
263
|
+
jsonOut([
|
|
264
|
+
"success": false,
|
|
265
|
+
"error": "Apple Foundation Model error: \(error.localizedDescription)"
|
|
266
|
+
])
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
#endif
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
Task {
|
|
273
|
+
await SynthuxAppleFMBridge.run()
|
|
274
|
+
exit(0)
|
|
275
|
+
}
|
|
276
|
+
RunLoop.main.run()
|
|
277
|
+
|