apple-llm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,456 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AppleLLM,
4
+ AppleLLMError,
5
+ captureScreenshot,
6
+ installCloudShortcut,
7
+ parseImageFlag,
8
+ probe
9
+ } from "./chunk-FQTRQ3KP.js";
10
+
11
+ // src/cli.ts
12
+ import { readFile } from "fs/promises";
13
+ function parseArgs(argv) {
14
+ const flags = { positional: [], image: [], document: [], tool: [] };
15
+ for (let i = 0; i < argv.length; i += 1) {
16
+ const arg = argv[i];
17
+ const take = (flag) => {
18
+ const v = argv[++i];
19
+ if (v === void 0 || v.startsWith("--")) {
20
+ throw new Error(`${flag} needs a value.
21
+
22
+ ${USAGE}`);
23
+ }
24
+ return v;
25
+ };
26
+ switch (arg) {
27
+ case "--tier":
28
+ flags.tier = take("--tier");
29
+ break;
30
+ case "--system":
31
+ flags.system = take("--system");
32
+ break;
33
+ case "--schema":
34
+ flags.schema = take("--schema");
35
+ break;
36
+ case "--temperature":
37
+ flags.temperature = take("--temperature");
38
+ break;
39
+ case "--max-tokens":
40
+ flags.maxTokens = take("--max-tokens");
41
+ break;
42
+ case "--web-search":
43
+ flags.webSearch = true;
44
+ break;
45
+ case "--force":
46
+ flags.force = true;
47
+ break;
48
+ case "--json":
49
+ flags.json = true;
50
+ break;
51
+ case "--image":
52
+ flags.image.push(take("--image"));
53
+ break;
54
+ case "--document":
55
+ flags.document.push(take("--document"));
56
+ break;
57
+ case "--tool":
58
+ flags.tool.push(take("--tool"));
59
+ break;
60
+ case "--session":
61
+ flags.session = take("--session");
62
+ break;
63
+ case "--stream":
64
+ flags.stream = true;
65
+ break;
66
+ case "--mode":
67
+ flags.mode = take("--mode");
68
+ break;
69
+ case "--use-case":
70
+ flags.useCase = take("--use-case");
71
+ break;
72
+ case "--guardrails":
73
+ flags.guardrails = take("--guardrails");
74
+ break;
75
+ case "--seed":
76
+ flags.seed = take("--seed");
77
+ break;
78
+ case "--greedy":
79
+ flags.greedy = true;
80
+ break;
81
+ default:
82
+ flags.positional.push(arg);
83
+ }
84
+ }
85
+ return flags;
86
+ }
87
+ async function readStdin() {
88
+ const chunks = [];
89
+ for await (const chunk of process.stdin) chunks.push(chunk);
90
+ return Buffer.concat(chunks).toString("utf8");
91
+ }
92
+ var KNOWN_TOOLS = /* @__PURE__ */ new Set(["ocr", "barcode", "spotlight"]);
93
+ function parseTools(values, usageError) {
94
+ if (values.length === 0) return void 0;
95
+ const out = [];
96
+ for (const value of values) {
97
+ for (const part of value.split(",").map((s) => s.trim()).filter(Boolean)) {
98
+ if (!KNOWN_TOOLS.has(part)) {
99
+ throw new Error(`--tool must be ocr, barcode, or spotlight (got "${part}").`);
100
+ }
101
+ if (!out.includes(part)) out.push(part);
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ var USAGE = `apple-llm \u2014 Apple's on-device and Private Cloud Compute models
107
+
108
+ apple-llm probe what this machine can do
109
+ apple-llm setup-cloud [--web-search] install the Shortcut the cloud tier needs
110
+ apple-llm run [options] <prompt|-> one completion ("-" reads stdin)
111
+ apple-llm count [options] <prompt|-> tokens this prompt costs, before sending
112
+ apple-llm ask-screen [options] <question|-> screenshot, then ask (device tier)
113
+ apple-llm history --session <id> show a conversation's mirrored turns
114
+ apple-llm reset [--session <id>] drop one conversation, or all
115
+ apple-llm rewrite|proofread|summarize|draft <text|-> Write with Siri presets
116
+
117
+ run options:
118
+ --tier device|cloud|auto default: auto
119
+ --system <text> system instructions
120
+ --schema <file.json> ask for JSON matching this JSON Schema
121
+ --temperature <n> default 0.4 (0 degenerates; see the README)
122
+ --max-tokens <n>
123
+ --image <path[::label]> attach an image, optionally labelled (repeatable; macOS 27+)
124
+ --document <path> inline a text document into the prompt (repeatable)
125
+ --tool ocr|barcode|spotlight built-in on-device tool (repeatable)
126
+ --session <id> continue a named conversation across calls
127
+ --stream print partials as they arrive (device text only)
128
+ --use-case contentTagging Apple's tagging-specialised model
129
+ --guardrails permissive relax guardrails for rewriting tasks
130
+ --seed <n> reproducible sampling (top-k, seeded)
131
+ --greedy greedy decoding (deterministic, but degenerates)
132
+ --web-search cloud tier only
133
+
134
+ ask-screen options: --mode interactive|window|fullscreen (default interactive),
135
+ plus --system, --session, --max-tokens.
136
+ `;
137
+ async function main() {
138
+ const [command, ...rest] = process.argv.slice(2);
139
+ let flags;
140
+ try {
141
+ flags = parseArgs(rest);
142
+ } catch (err) {
143
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
144
+ `);
145
+ return 2;
146
+ }
147
+ const usageError = (message) => {
148
+ process.stderr.write(`${message}
149
+
150
+ ${USAGE}`);
151
+ return 2;
152
+ };
153
+ if (command === void 0 || command === "--help" || command === "-h" || command === "help") {
154
+ process.stdout.write(USAGE);
155
+ return 0;
156
+ }
157
+ if (command === "probe") {
158
+ const result = await probe((p) => process.stderr.write(` ${p.status}
159
+ `));
160
+ if (flags.json === true) {
161
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
162
+ `);
163
+ return result.device.available || result.cloud.available ? 0 : 1;
164
+ }
165
+ const d = result.device;
166
+ process.stdout.write("on-device\n");
167
+ process.stdout.write(` available ${d.available}
168
+ `);
169
+ if (d.variant !== void 0) process.stdout.write(` variant ${d.variant}
170
+ `);
171
+ if (d.contextSize !== void 0) process.stdout.write(` context ${d.contextSize} tokens
172
+ `);
173
+ if (d.capabilities !== void 0) {
174
+ const on = Object.entries(d.capabilities).filter(([, v]) => v).map(([k]) => k);
175
+ process.stdout.write(` supports ${on.join(", ") || "none reported"}
176
+ `);
177
+ }
178
+ if (d.useCases !== void 0) process.stdout.write(` use cases ${d.useCases.join(", ")}
179
+ `);
180
+ if (d.reason !== void 0) process.stdout.write(` reason ${d.reason}
181
+ `);
182
+ const c = result.cloud;
183
+ process.stdout.write("\ncloud (private cloud compute)\n");
184
+ process.stdout.write(` available ${c.available}
185
+ `);
186
+ process.stdout.write(` installed ${c.installed === true}
187
+ `);
188
+ if (c.contextSize !== void 0) process.stdout.write(` context ${c.contextSize} tokens
189
+ `);
190
+ if (c.quota !== void 0) {
191
+ const approaching = c.quota.approachingLimit === true ? " (approaching limit)" : "";
192
+ process.stdout.write(` quota ${c.quota.status}${approaching}
193
+ `);
194
+ if (c.quota.resetDate !== void 0) process.stdout.write(` resets ${c.quota.resetDate}
195
+ `);
196
+ }
197
+ if (c.reason !== void 0) process.stdout.write(` reason ${c.reason}
198
+ `);
199
+ return d.available || c.available ? 0 : 1;
200
+ }
201
+ if (command === "setup-cloud") {
202
+ await installCloudShortcut((p) => process.stderr.write(` ${p.status}
203
+ `), {
204
+ force: flags.force,
205
+ webSearch: flags.webSearch
206
+ });
207
+ return 0;
208
+ }
209
+ if (command === "count") {
210
+ const arg = flags.positional[0];
211
+ if (arg === void 0) {
212
+ process.stderr.write('count needs a prompt, or "-" to read stdin.\n');
213
+ return 2;
214
+ }
215
+ const prompt = arg === "-" ? await readStdin() : arg;
216
+ const llm = new AppleLLM({ tier: "device" });
217
+ let tools;
218
+ try {
219
+ tools = parseTools(flags.tool, usageError);
220
+ } catch (err) {
221
+ return usageError(err instanceof Error ? err.message : String(err));
222
+ }
223
+ try {
224
+ const { tokens, contextSize } = await llm.countTokens(prompt, {
225
+ system: flags.system,
226
+ images: flags.image.length > 0 ? flags.image.map(parseImageFlag) : void 0,
227
+ tools
228
+ });
229
+ const pct = contextSize > 0 ? Math.round(tokens / contextSize * 100) : 0;
230
+ process.stdout.write(`${tokens} tokens of ${contextSize} (${pct}%)
231
+ `);
232
+ return tokens > contextSize ? 1 : 0;
233
+ } finally {
234
+ llm.close();
235
+ }
236
+ }
237
+ if (command === "history") {
238
+ if (flags.session === void 0) return usageError("history needs --session <id>.");
239
+ const llm = new AppleLLM({ tier: "device" });
240
+ try {
241
+ const { instructions, history } = await llm.history(flags.session);
242
+ if (flags.json === true) {
243
+ process.stdout.write(`${JSON.stringify({ sessionId: flags.session, instructions, history }, null, 2)}
244
+ `);
245
+ return 0;
246
+ }
247
+ for (const turn of history) {
248
+ process.stdout.write(`${turn.role === "user" ? "you" : "model"}: ${turn.content}
249
+ `);
250
+ }
251
+ return 0;
252
+ } finally {
253
+ llm.close();
254
+ }
255
+ }
256
+ if (command === "reset") {
257
+ const llm = new AppleLLM({ tier: "device" });
258
+ try {
259
+ await llm.resetSession(flags.session);
260
+ process.stdout.write(flags.session ? `reset ${flags.session}
261
+ ` : "reset all sessions\n");
262
+ return 0;
263
+ } finally {
264
+ llm.close();
265
+ }
266
+ }
267
+ if (command === "rewrite" || command === "proofread" || command === "summarize" || command === "draft") {
268
+ const arg = flags.positional[0];
269
+ if (arg === void 0) {
270
+ process.stderr.write(`${command} needs text, or "-" to read stdin.
271
+ `);
272
+ return 2;
273
+ }
274
+ const text = arg === "-" ? await readStdin() : arg;
275
+ const llm = new AppleLLM({
276
+ tier: "device",
277
+ guardrails: "permissive",
278
+ onProgress: (p) => process.stderr.write(` ${p.status}
279
+ `)
280
+ });
281
+ try {
282
+ const out = command === "rewrite" ? await llm.rewrite(text, { system: flags.system }) : command === "proofread" ? await llm.proofread(text, { system: flags.system }) : command === "summarize" ? await llm.summarize(text, { system: flags.system }) : await llm.draft(text, { system: flags.system });
283
+ process.stdout.write(`${out}
284
+ `);
285
+ return 0;
286
+ } finally {
287
+ llm.close();
288
+ }
289
+ }
290
+ if (command === "ask-screen") {
291
+ const arg = flags.positional[0];
292
+ if (arg === void 0) {
293
+ process.stderr.write('ask-screen needs a question, or "-" to read stdin.\n');
294
+ return 2;
295
+ }
296
+ if (flags.mode !== void 0 && !["interactive", "window", "fullscreen"].includes(flags.mode)) {
297
+ return usageError(`--mode must be interactive, window, or fullscreen (got "${flags.mode}").`);
298
+ }
299
+ const question = arg === "-" ? await readStdin() : arg;
300
+ const maxTokens = flags.maxTokens === void 0 ? void 0 : Number(flags.maxTokens);
301
+ if (maxTokens !== void 0 && Number.isNaN(maxTokens)) {
302
+ return usageError(`--max-tokens must be a number (got "${flags.maxTokens}").`);
303
+ }
304
+ process.stderr.write(" select a screen region (Esc cancels)\n");
305
+ const shot = await captureScreenshot(
306
+ flags.mode ?? "interactive"
307
+ );
308
+ const llm = new AppleLLM({
309
+ tier: "device",
310
+ maxTokens,
311
+ onProgress: (p) => process.stderr.write(` ${p.status}
312
+ `)
313
+ });
314
+ try {
315
+ const out = await llm.text(question, {
316
+ system: flags.system,
317
+ sessionId: flags.session,
318
+ images: [...flags.image.map(parseImageFlag), shot]
319
+ });
320
+ process.stdout.write(`${out}
321
+ `);
322
+ return 0;
323
+ } finally {
324
+ const { rm } = await import("fs/promises");
325
+ await rm(shot, { force: true }).catch(() => void 0);
326
+ llm.close();
327
+ }
328
+ }
329
+ if (command === "run") {
330
+ const arg = flags.positional[0];
331
+ if (arg === void 0) {
332
+ process.stderr.write('run needs a prompt, or "-" to read stdin.\n');
333
+ return 2;
334
+ }
335
+ if (flags.tier !== void 0 && flags.tier !== "device" && flags.tier !== "cloud" && flags.tier !== "auto") {
336
+ return usageError(`--tier must be device, cloud, or auto (got "${flags.tier}").`);
337
+ }
338
+ const temperature = flags.temperature === void 0 ? void 0 : Number(flags.temperature);
339
+ if (temperature !== void 0 && Number.isNaN(temperature)) {
340
+ return usageError(`--temperature must be a number (got "${flags.temperature}").`);
341
+ }
342
+ const maxTokens = flags.maxTokens === void 0 ? void 0 : Number(flags.maxTokens);
343
+ if (maxTokens !== void 0 && Number.isNaN(maxTokens)) {
344
+ return usageError(`--max-tokens must be a number (got "${flags.maxTokens}").`);
345
+ }
346
+ const seed = flags.seed === void 0 ? void 0 : Number(flags.seed);
347
+ if (seed !== void 0 && Number.isNaN(seed)) {
348
+ return usageError(`--seed must be a number (got "${flags.seed}").`);
349
+ }
350
+ if (flags.useCase !== void 0 && flags.useCase !== "general" && flags.useCase !== "contentTagging") {
351
+ return usageError(`--use-case must be general or contentTagging (got "${flags.useCase}").`);
352
+ }
353
+ if (flags.guardrails !== void 0 && flags.guardrails !== "default" && flags.guardrails !== "permissive") {
354
+ return usageError(`--guardrails must be default or permissive (got "${flags.guardrails}").`);
355
+ }
356
+ const prompt = arg === "-" ? await readStdin() : arg;
357
+ const tier = flags.tier ?? "auto";
358
+ let tools;
359
+ try {
360
+ tools = parseTools(flags.tool, usageError);
361
+ } catch (err) {
362
+ return usageError(err instanceof Error ? err.message : String(err));
363
+ }
364
+ const sampling = flags.greedy === true ? { mode: "greedy" } : seed !== void 0 ? { mode: "topK", k: 50, seed } : void 0;
365
+ const llm = new AppleLLM({
366
+ tier,
367
+ temperature,
368
+ maxTokens,
369
+ useCase: flags.useCase,
370
+ guardrails: flags.guardrails,
371
+ sampling,
372
+ onProgress: (p) => process.stderr.write(` ${p.status}
373
+ `)
374
+ });
375
+ const images = flags.image.length > 0 ? flags.image.map(parseImageFlag) : void 0;
376
+ const documents = flags.document.length > 0 ? flags.document : void 0;
377
+ try {
378
+ if (flags.schema !== void 0) {
379
+ if (flags.stream === true) {
380
+ return usageError("--stream is text only; schemas need a complete response.");
381
+ }
382
+ let schemaText;
383
+ try {
384
+ schemaText = await readFile(flags.schema, "utf8");
385
+ } catch (err) {
386
+ throw new AppleLLMError(
387
+ `Could not read schema file "${flags.schema}": ${err instanceof Error ? err.message : String(err)}`
388
+ );
389
+ }
390
+ let schema;
391
+ try {
392
+ schema = JSON.parse(schemaText);
393
+ } catch (err) {
394
+ throw new AppleLLMError(
395
+ `Schema file "${flags.schema}" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
396
+ );
397
+ }
398
+ const out = await llm.json(prompt, {
399
+ schema,
400
+ system: flags.system,
401
+ webSearch: flags.webSearch,
402
+ images,
403
+ documents,
404
+ sessionId: flags.session,
405
+ tools
406
+ });
407
+ process.stdout.write(`${JSON.stringify(out, null, 2)}
408
+ `);
409
+ } else if (flags.stream === true) {
410
+ if (tier === "cloud") {
411
+ return usageError("--stream needs the on-device tier.");
412
+ }
413
+ const out = await llm.stream(prompt, {
414
+ system: flags.system,
415
+ images,
416
+ documents,
417
+ sessionId: flags.session,
418
+ tools,
419
+ onDelta: (delta) => process.stdout.write(delta)
420
+ });
421
+ process.stdout.write("\n");
422
+ void out;
423
+ } else {
424
+ const out = await llm.text(prompt, {
425
+ system: flags.system,
426
+ webSearch: flags.webSearch,
427
+ images,
428
+ documents,
429
+ sessionId: flags.session,
430
+ tools
431
+ });
432
+ process.stdout.write(`${out}
433
+ `);
434
+ }
435
+ } finally {
436
+ llm.close();
437
+ }
438
+ return 0;
439
+ }
440
+ process.stderr.write(`Unknown command "${command}".
441
+
442
+ ${USAGE}`);
443
+ return 2;
444
+ }
445
+ main().then((code) => {
446
+ process.exitCode = code;
447
+ }).catch((err) => {
448
+ if (err instanceof AppleLLMError) {
449
+ process.stderr.write(`${err.name}: ${err.message}
450
+ `);
451
+ } else {
452
+ process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}
453
+ `);
454
+ }
455
+ process.exitCode = 1;
456
+ });