apple-notes-mcp 2.9.4 → 2.9.6
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 +85 -2
- package/build/index.js +964 -71
- package/native/public-helper/apple-notes-public-helper.swift +257 -0
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// apple-notes-public-helper
|
|
2
|
+
//
|
|
3
|
+
// A small command-line helper for apple-notes-mcp that uses PUBLIC Apple
|
|
4
|
+
// frameworks only. It never opens the Notes database and never writes to the
|
|
5
|
+
// Notes group container: the TypeScript server reads the bytes it needs
|
|
6
|
+
// (read-only) and hands them over on stdin.
|
|
7
|
+
//
|
|
8
|
+
// Protocol: one JSON object on stdin, one JSON object on stdout.
|
|
9
|
+
// request: {"protocol": 1, "action": "<name>", ...fields}
|
|
10
|
+
// success: {"status": "ok", ...}
|
|
11
|
+
// failure: {"status": "error", "code": "<code>", "message": "<text>"}
|
|
12
|
+
//
|
|
13
|
+
// Actions:
|
|
14
|
+
// hello protocol version, embedded source digest, action list
|
|
15
|
+
// decode_drawing PencilKit drawing bytes (base64) -> strokes
|
|
16
|
+
// encode_drawing strokes -> PencilKit drawing bytes (base64); used to build
|
|
17
|
+
// synthetic test fixtures, never to write to Notes
|
|
18
|
+
//
|
|
19
|
+
// Build (done by `apple-notes-mcp setup --public-helper`, which also generates
|
|
20
|
+
// the one-line source-digest file that defines helperSourceSHA256 and the
|
|
21
|
+
// Info.plist linked into __TEXT,__info_plist; PencilKit needs a bundle id):
|
|
22
|
+
// xcrun swiftc -O -parse-as-library -framework AppKit -framework PencilKit \
|
|
23
|
+
// -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker Info.plist \
|
|
24
|
+
// apple-notes-public-helper.swift source-digest.swift -o apple-notes-public-helper
|
|
25
|
+
|
|
26
|
+
import AppKit
|
|
27
|
+
import Foundation
|
|
28
|
+
import PencilKit
|
|
29
|
+
|
|
30
|
+
let protocolVersion = 1
|
|
31
|
+
let maxInputBytes = 96 * 1024 * 1024
|
|
32
|
+
let maxStrokes = 20_000
|
|
33
|
+
let maxPoints = 1_000_000
|
|
34
|
+
|
|
35
|
+
// MARK: - Output
|
|
36
|
+
|
|
37
|
+
struct HelperFailure: Error {
|
|
38
|
+
let code: String
|
|
39
|
+
let message: String
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func writeJSON(_ object: [String: Any]) {
|
|
43
|
+
let data: Data
|
|
44
|
+
if JSONSerialization.isValidJSONObject(object),
|
|
45
|
+
let encoded = try? JSONSerialization.data(withJSONObject: object, options: [.sortedKeys]) {
|
|
46
|
+
data = encoded
|
|
47
|
+
} else {
|
|
48
|
+
data = Data(#"{"code":"encode_failed","message":"response was not JSON-encodable","status":"error"}"#.utf8)
|
|
49
|
+
}
|
|
50
|
+
FileHandle.standardOutput.write(data)
|
|
51
|
+
FileHandle.standardOutput.write(Data([0x0A]))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func finish(_ object: [String: Any]) -> Never {
|
|
55
|
+
writeJSON(object)
|
|
56
|
+
exit(object["status"] as? String == "ok" ? 0 : 1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func fail(_ failure: HelperFailure) -> Never {
|
|
60
|
+
finish(["status": "error", "code": failure.code, "message": failure.message])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Round to two decimals so large drawings stay compact on the wire.
|
|
64
|
+
func rounded(_ value: CGFloat) -> Double {
|
|
65
|
+
(Double(value) * 100).rounded() / 100
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// MARK: - Drawings
|
|
69
|
+
|
|
70
|
+
func rgba(_ color: NSColor) -> [String: Any] {
|
|
71
|
+
let srgb = color.usingColorSpace(.sRGB) ?? color
|
|
72
|
+
func channel(_ value: CGFloat) -> Int { Int((min(max(value, 0), 1) * 255).rounded()) }
|
|
73
|
+
return [
|
|
74
|
+
"red": channel(srgb.redComponent),
|
|
75
|
+
"green": channel(srgb.greenComponent),
|
|
76
|
+
"blue": channel(srgb.blueComponent),
|
|
77
|
+
"alpha": rounded(srgb.alphaComponent),
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func decodeDrawing(_ request: [String: Any]) throws -> [String: Any] {
|
|
82
|
+
guard let encoded = request["dataBase64"] as? String,
|
|
83
|
+
let data = Data(base64Encoded: encoded)
|
|
84
|
+
else { throw HelperFailure(code: "invalid_request", message: "dataBase64 is required") }
|
|
85
|
+
if data.isEmpty { throw HelperFailure(code: "invalid_request", message: "drawing data is empty") }
|
|
86
|
+
let includePoints = request["includePoints"] as? Bool ?? true
|
|
87
|
+
|
|
88
|
+
let drawing: PKDrawing
|
|
89
|
+
do {
|
|
90
|
+
drawing = try PKDrawing(data: data)
|
|
91
|
+
} catch {
|
|
92
|
+
throw HelperFailure(
|
|
93
|
+
code: "undecodable",
|
|
94
|
+
message: "PencilKit could not read this drawing: \(error.localizedDescription)"
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var strokes: [[String: Any]] = []
|
|
99
|
+
var pointBudget = maxPoints
|
|
100
|
+
var truncated = false
|
|
101
|
+
for stroke in drawing.strokes {
|
|
102
|
+
if strokes.count >= maxStrokes || pointBudget <= 0 {
|
|
103
|
+
truncated = true
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
let path = stroke.path
|
|
107
|
+
let transform = stroke.transform
|
|
108
|
+
var points: [[String: Any]] = []
|
|
109
|
+
var widthSum: CGFloat = 0
|
|
110
|
+
var count = 0
|
|
111
|
+
for point in path {
|
|
112
|
+
count += 1
|
|
113
|
+
widthSum += point.size.width
|
|
114
|
+
if includePoints {
|
|
115
|
+
if pointBudget <= 0 {
|
|
116
|
+
truncated = true
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
pointBudget -= 1
|
|
120
|
+
let location = point.location.applying(transform)
|
|
121
|
+
points.append([
|
|
122
|
+
"x": rounded(location.x),
|
|
123
|
+
"y": rounded(location.y),
|
|
124
|
+
"width": rounded(point.size.width),
|
|
125
|
+
"opacity": rounded(point.opacity),
|
|
126
|
+
"force": rounded(point.force),
|
|
127
|
+
])
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
let bounds = stroke.renderBounds
|
|
131
|
+
var entry: [String: Any] = [
|
|
132
|
+
"inkType": stroke.ink.inkType.rawValue,
|
|
133
|
+
"color": rgba(stroke.ink.color),
|
|
134
|
+
"width": count > 0 ? rounded(widthSum / CGFloat(count)) : 0,
|
|
135
|
+
"pointCount": count,
|
|
136
|
+
"bounds": [
|
|
137
|
+
"x": rounded(bounds.origin.x), "y": rounded(bounds.origin.y),
|
|
138
|
+
"width": rounded(bounds.size.width), "height": rounded(bounds.size.height),
|
|
139
|
+
],
|
|
140
|
+
]
|
|
141
|
+
if includePoints { entry["points"] = points }
|
|
142
|
+
if !transform.isIdentity { entry["transformApplied"] = true }
|
|
143
|
+
strokes.append(entry)
|
|
144
|
+
}
|
|
145
|
+
let bounds = drawing.bounds
|
|
146
|
+
return [
|
|
147
|
+
"status": "ok",
|
|
148
|
+
"strokeCount": drawing.strokes.count,
|
|
149
|
+
"strokes": strokes,
|
|
150
|
+
"truncated": truncated,
|
|
151
|
+
"bounds": [
|
|
152
|
+
"x": rounded(bounds.origin.x), "y": rounded(bounds.origin.y),
|
|
153
|
+
"width": rounded(bounds.size.width), "height": rounded(bounds.size.height),
|
|
154
|
+
],
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func inkType(named name: String) -> PKInk.InkType {
|
|
159
|
+
switch name {
|
|
160
|
+
case "com.apple.ink.marker", "marker": return .marker
|
|
161
|
+
case "com.apple.ink.pencil", "pencil": return .pencil
|
|
162
|
+
default: return .pen
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func encodeDrawing(_ request: [String: Any]) throws -> [String: Any] {
|
|
167
|
+
guard let specs = request["strokes"] as? [[String: Any]], !specs.isEmpty else {
|
|
168
|
+
throw HelperFailure(code: "invalid_request", message: "strokes must be a non-empty array")
|
|
169
|
+
}
|
|
170
|
+
if specs.count > maxStrokes {
|
|
171
|
+
throw HelperFailure(code: "invalid_request", message: "too many strokes")
|
|
172
|
+
}
|
|
173
|
+
var strokes: [PKStroke] = []
|
|
174
|
+
let created = Date(timeIntervalSince1970: 0)
|
|
175
|
+
for spec in specs {
|
|
176
|
+
guard let rawPoints = spec["points"] as? [[String: Any]], rawPoints.count >= 2 else {
|
|
177
|
+
throw HelperFailure(code: "invalid_request", message: "each stroke needs at least two points")
|
|
178
|
+
}
|
|
179
|
+
let width = CGFloat((spec["width"] as? NSNumber)?.doubleValue ?? 3)
|
|
180
|
+
let color = spec["color"] as? [String: Any] ?? [:]
|
|
181
|
+
func component(_ key: String, _ fallback: Double) -> CGFloat {
|
|
182
|
+
CGFloat((color[key] as? NSNumber)?.doubleValue ?? fallback)
|
|
183
|
+
}
|
|
184
|
+
let ink = PKInk(
|
|
185
|
+
inkType(named: spec["inkType"] as? String ?? "pen"),
|
|
186
|
+
color: NSColor(
|
|
187
|
+
srgbRed: component("red", 0) / 255,
|
|
188
|
+
green: component("green", 0) / 255,
|
|
189
|
+
blue: component("blue", 0) / 255,
|
|
190
|
+
alpha: component("alpha", 1)
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
let last = Double(rawPoints.count - 1)
|
|
194
|
+
let controlPoints = rawPoints.enumerated().map { index, point in
|
|
195
|
+
PKStrokePoint(
|
|
196
|
+
location: CGPoint(
|
|
197
|
+
x: (point["x"] as? NSNumber)?.doubleValue ?? 0,
|
|
198
|
+
y: (point["y"] as? NSNumber)?.doubleValue ?? 0
|
|
199
|
+
),
|
|
200
|
+
timeOffset: Double(index) / last,
|
|
201
|
+
size: CGSize(width: width, height: width),
|
|
202
|
+
opacity: 1,
|
|
203
|
+
force: 1,
|
|
204
|
+
azimuth: 0,
|
|
205
|
+
altitude: .pi / 2
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
strokes.append(PKStroke(ink: ink, path: PKStrokePath(controlPoints: controlPoints, creationDate: created)))
|
|
209
|
+
}
|
|
210
|
+
let data = PKDrawing(strokes: strokes).dataRepresentation()
|
|
211
|
+
return ["status": "ok", "strokeCount": strokes.count, "dataBase64": data.base64EncodedString()]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// MARK: - Dispatch
|
|
215
|
+
|
|
216
|
+
let actions = ["hello", "decode_drawing", "encode_drawing"]
|
|
217
|
+
|
|
218
|
+
func handle(_ request: [String: Any]) throws -> [String: Any] {
|
|
219
|
+
guard (request["protocol"] as? NSNumber)?.intValue == protocolVersion else {
|
|
220
|
+
throw HelperFailure(code: "protocol_mismatch", message: "expected protocol \(protocolVersion)")
|
|
221
|
+
}
|
|
222
|
+
switch request["action"] as? String {
|
|
223
|
+
case "hello":
|
|
224
|
+
return [
|
|
225
|
+
"status": "ok",
|
|
226
|
+
"protocolVersion": protocolVersion,
|
|
227
|
+
"sourceSha256": helperSourceSHA256,
|
|
228
|
+
"actions": actions,
|
|
229
|
+
]
|
|
230
|
+
case "decode_drawing":
|
|
231
|
+
return try decodeDrawing(request)
|
|
232
|
+
case "encode_drawing":
|
|
233
|
+
return try encodeDrawing(request)
|
|
234
|
+
default:
|
|
235
|
+
throw HelperFailure(code: "unknown_action", message: "unknown action")
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@main
|
|
240
|
+
struct PublicHelper {
|
|
241
|
+
static func main() {
|
|
242
|
+
let input = FileHandle.standardInput.readDataToEndOfFile()
|
|
243
|
+
if input.count > maxInputBytes {
|
|
244
|
+
fail(HelperFailure(code: "invalid_request", message: "request exceeds \(maxInputBytes) bytes"))
|
|
245
|
+
}
|
|
246
|
+
guard let request = (try? JSONSerialization.jsonObject(with: input)) as? [String: Any] else {
|
|
247
|
+
fail(HelperFailure(code: "invalid_request", message: "stdin must be one JSON object"))
|
|
248
|
+
}
|
|
249
|
+
do {
|
|
250
|
+
finish(try handle(request))
|
|
251
|
+
} catch let failure as HelperFailure {
|
|
252
|
+
fail(failure)
|
|
253
|
+
} catch {
|
|
254
|
+
fail(HelperFailure(code: "internal_error", message: error.localizedDescription))
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
package/package.json
CHANGED