@thenavidm/apple-photos-mcp-cli 1.0.2 → 1.0.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/LICENSE +1 -1
- package/README.md +129 -32
- package/SKILL.md +16 -0
- package/lib/bridge.d.ts +23 -12
- package/lib/bridge.js +48 -21
- package/lib/bridge.js.map +1 -1
- package/lib/cli.d.ts +6 -14
- package/lib/cli.js +72 -94
- package/lib/cli.js.map +1 -1
- package/lib/config.d.ts +5 -1
- package/lib/config.js +32 -2
- package/lib/config.js.map +1 -1
- package/lib/index.js +6 -6
- package/lib/index.js.map +1 -1
- package/lib/tools/index.js +22 -7
- package/lib/tools/index.js.map +1 -1
- package/lib/tools/kit.d.ts +6 -2
- package/lib/tools/kit.js +29 -6
- package/lib/tools/kit.js.map +1 -1
- package/package.json +3 -3
- package/pyproject.toml +3 -3
- package/src/apple_photos_mcp/doctor.py +2 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,27 +1,120 @@
|
|
|
1
1
|
<img src="https://cdn.navid.media/connectors/apple-photos-icon.png" alt="Apple Photos" width="88">
|
|
2
2
|
|
|
3
|
-
# Apple Photos MCP
|
|
3
|
+
# Apple Photos MCP + CLI
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@thenavidm/apple-photos-mcp-cli)
|
|
6
|
+
[](./LICENSE)
|
|
7
7
|
[](https://youtube.com/@thenavidm?sub_confirmation=1)
|
|
8
8
|
[](https://x.com/thenavidm)
|
|
9
9
|
[](https://linkedin.com/in/thenavidm)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Apple Photos MCP server and CLI for Claude Code and AI agents. 13 tools to search, look at, organise and export your own library, entirely on your Mac.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Give any AI agent real access to your own Apple Photos library, so it can find, see, organise and export your photos instead of guessing.
|
|
14
|
+
|
|
15
|
+
Built and maintained by [Navid Moazzez](https://navid.me).
|
|
14
16
|
|
|
15
17
|
<img src="https://cdn.navid.media/repos/apple-photos-mcp.gif?v=1" alt="Claude Code using the Apple Photos MCP server" width="520">
|
|
16
18
|
|
|
17
19
|
Everything runs on your Mac. There is no backend.
|
|
18
20
|
|
|
21
|
+
## Two ways to use it
|
|
22
|
+
|
|
23
|
+
### Command line
|
|
24
|
+
|
|
25
|
+
`apple-photos-cli` in your terminal, for scripting, cron, pipes, or a quick
|
|
26
|
+
question without opening anything:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
apple-photos-cli # every command, one line each
|
|
30
|
+
apple-photos-cli library-stats --json # real totals, one call
|
|
31
|
+
apple-photos-cli search-photos "sunset" --limit 5 --screenshots exclude
|
|
32
|
+
apple-photos-cli photo-info --refs IMG_2073.MOV
|
|
33
|
+
apple-photos-cli export-originals --refs <uuid> --directory ./out
|
|
34
|
+
apple-photos-cli <command> --help # what any command takes
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`--json` gives JSON, `--compact` puts it on one line, `--select total,videos`
|
|
38
|
+
keeps only the fields you name, and errors are JSON on stderr whichever you
|
|
39
|
+
pick. `--confirm` is the shell spelling of the confirmation archiving requires.
|
|
40
|
+
|
|
41
|
+
### MCP server, for AI agents
|
|
42
|
+
|
|
43
|
+
`apple-photos-mcp` is what Claude Code, Claude Desktop, Cursor and the rest
|
|
44
|
+
launch. You never run it by hand:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
claude mcp add apple-photos -- npx -y @thenavidm/apple-photos-mcp-cli@latest
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then just ask: _"which receipts do I have from Vietnam?"_
|
|
51
|
+
|
|
52
|
+
### Which one
|
|
53
|
+
|
|
54
|
+
| What you are doing | Use |
|
|
55
|
+
|---|---|
|
|
56
|
+
| Inside a conversation with an agent | MCP |
|
|
57
|
+
| Piping, scripting, cron, CI | CLI |
|
|
58
|
+
| A one-off question in a terminal | CLI |
|
|
59
|
+
|
|
60
|
+
They are the same program reading the same tool definitions, so anything one
|
|
61
|
+
can do, the other can. A test asserts they cannot drift.
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
Every tool is both a command and an MCP tool, with the same name. The command is
|
|
66
|
+
the tool name with dashes.
|
|
67
|
+
|
|
68
|
+
| Capability | CLI command | MCP tool |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| Search the whole library | `apple-photos-cli search-photos` | `search_photos` |
|
|
71
|
+
| Actually look at photos | `apple-photos-cli look-at-photos` | `look_at_photos` |
|
|
72
|
+
| Full metadata, camera and lens | `apple-photos-cli photo-info` | `photo_info` |
|
|
73
|
+
| Real totals in one call | `apple-photos-cli library-stats` | `library_stats` |
|
|
74
|
+
| What Apple can search for | `apple-photos-cli list-vocabulary` | `list_vocabulary` |
|
|
75
|
+
| Export originals to disk | `apple-photos-cli export-originals` | `export_originals` |
|
|
76
|
+
| Favourite or unfavourite | `apple-photos-cli favorite-photos` | `favorite_photos` |
|
|
77
|
+
| Title and description | `apple-photos-cli set-photo-title` / `set-photo-description` | `set_photo_title` / `set_photo_description` |
|
|
78
|
+
| Keywords and albums | `apple-photos-cli add-keywords` / `add-to-album` | `add_keywords` / `add_to_album` |
|
|
79
|
+
| Move into an archive album | `apple-photos-cli archive-photos` | `archive_photos` |
|
|
80
|
+
| Check the setup | `apple-photos-cli doctor` | `doctor` |
|
|
81
|
+
| Find the right command | `apple-photos-cli which "..."` | not a tool |
|
|
82
|
+
|
|
83
|
+
All 13 with their arguments are in [section 6](#6-tools-).
|
|
84
|
+
|
|
85
|
+
## Output and exit codes
|
|
86
|
+
|
|
87
|
+
Results on stdout, errors on stderr as JSON, so one parse handles both.
|
|
88
|
+
|
|
89
|
+
| Flag | Result |
|
|
90
|
+
|---|---|
|
|
91
|
+
| none | pretty JSON |
|
|
92
|
+
| `--json` | JSON, always |
|
|
93
|
+
| `--compact` | the same JSON on one line |
|
|
94
|
+
| `--select a,b.c` | keep only these fields |
|
|
95
|
+
|
|
96
|
+
| Code | Means |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `0` | it worked |
|
|
99
|
+
| `1` | it failed: the engine refused, a guard blocked it, nothing matched |
|
|
100
|
+
| `2` | it was typed wrong: a missing flag, a bad value, an unknown option |
|
|
101
|
+
|
|
102
|
+
## Which surface, and what each costs
|
|
103
|
+
|
|
104
|
+
An MCP server is charged on every turn; a CLI costs nothing until it is called.
|
|
105
|
+
|
|
106
|
+
The `tools/list` payload for these 13 tools is about **2,800 tokens**, plus the
|
|
107
|
+
server instructions, on every turn of every conversation whether you use it or
|
|
108
|
+
not. The CLI is free until you run something, which is why both exist.
|
|
109
|
+
|
|
19
110
|
## Contents
|
|
20
111
|
|
|
21
112
|
| | Section | |
|
|
22
113
|
|---|---|---|
|
|
114
|
+
| | [Features](#features) | Every tool, both surfaces |
|
|
115
|
+
| | [Output and exit codes](#output-and-exit-codes) | What scripts branch on |
|
|
23
116
|
| 1 | [What you can ask it](#1-what-you-can-ask-it-) | Real prompts, not features |
|
|
24
|
-
| 2 | [Quick install](#2-quick-install-) |
|
|
117
|
+
| 2 | [Quick install](#2-quick-install-) | One command, no credential |
|
|
25
118
|
| 3 | [Setup](#3-setup-) | One permission, once |
|
|
26
119
|
| 4 | [Connect your client](#4-connect-your-client-) | Every client, copy and paste |
|
|
27
120
|
| 5 | [Check it worked](#5-check-it-worked-) | `doctor` |
|
|
@@ -49,21 +142,26 @@ The first one is the point. Your library already knows what is in every photo, b
|
|
|
49
142
|
|
|
50
143
|
## 2. Quick install ⚡
|
|
51
144
|
|
|
52
|
-
macOS, and
|
|
53
|
-
|
|
54
|
-
> **Not on PyPI yet.** Install it from GitHub until it is published. The command below works today.
|
|
145
|
+
macOS, and Node 20 or newer.
|
|
55
146
|
|
|
56
147
|
```bash
|
|
57
|
-
|
|
148
|
+
npx -y @thenavidm/apple-photos-mcp-cli@latest --version
|
|
58
149
|
```
|
|
59
150
|
|
|
60
|
-
|
|
151
|
+
That is the whole install. `npx` fetches it on demand, so there is nothing to
|
|
152
|
+
update later.
|
|
153
|
+
|
|
154
|
+
The engine underneath is Python, because `osxphotos` and `photoscript` are the
|
|
155
|
+
only libraries that can read a Photos library and both are Python-only. You do
|
|
156
|
+
not install them: [uv](https://docs.astral.sh/uv/) fetches them on first run and
|
|
157
|
+
caches them. If you do not have uv:
|
|
61
158
|
|
|
62
159
|
```bash
|
|
63
160
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
64
161
|
```
|
|
65
162
|
|
|
66
|
-
|
|
163
|
+
No account, no API key, no credential. macOS will ask for permission the first
|
|
164
|
+
time something reads the library.
|
|
67
165
|
|
|
68
166
|
## 3. Setup 🔑
|
|
69
167
|
|
|
@@ -107,7 +205,7 @@ Set up apple-photos-mcp for me.
|
|
|
107
205
|
|
|
108
206
|
```bash
|
|
109
207
|
claude mcp add apple-photos -s user \
|
|
110
|
-
--
|
|
208
|
+
-- npx -y @thenavidm/apple-photos-mcp-cli@latest
|
|
111
209
|
```
|
|
112
210
|
|
|
113
211
|
`-s user` makes it available in every project rather than just the current one.
|
|
@@ -138,8 +236,8 @@ Apple Photos only exists on macOS, so there is no Windows or Linux path here.
|
|
|
138
236
|
{
|
|
139
237
|
"mcpServers": {
|
|
140
238
|
"apple-photos": {
|
|
141
|
-
"command": "
|
|
142
|
-
"args": ["
|
|
239
|
+
"command": "npx",
|
|
240
|
+
"args": ["-y", "@thenavidm/apple-photos-mcp-cli@latest"]
|
|
143
241
|
}
|
|
144
242
|
}
|
|
145
243
|
}
|
|
@@ -147,8 +245,8 @@ Apple Photos only exists on macOS, so there is no Windows or Linux path here.
|
|
|
147
245
|
|
|
148
246
|
**If it already has other servers**, add only the `"apple-photos"` block inside the existing `"mcpServers"` object, and put a comma after the previous server's closing brace. One misplaced comma invalidates the file, and then every server disappears, not just this one.
|
|
149
247
|
|
|
150
|
-
>
|
|
151
|
-
> Claude Desktop does not inherit your shell PATH. If `
|
|
248
|
+
> [!TIP]
|
|
249
|
+
> Claude Desktop does not inherit your shell PATH. If `npx` is not found, run `which npx` in a terminal and use that absolute path as `"command"`.
|
|
152
250
|
|
|
153
251
|
Quit Claude Desktop completely with Cmd+Q and reopen it.
|
|
154
252
|
|
|
@@ -177,8 +275,8 @@ For one project instead of globally, use `.cursor/mcp.json` in that project.
|
|
|
177
275
|
"servers": {
|
|
178
276
|
"apple-photos": {
|
|
179
277
|
"type": "stdio",
|
|
180
|
-
"command": "
|
|
181
|
-
"args": ["
|
|
278
|
+
"command": "npx",
|
|
279
|
+
"args": ["-y", "@thenavidm/apple-photos-mcp-cli@latest"]
|
|
182
280
|
}
|
|
183
281
|
}
|
|
184
282
|
}
|
|
@@ -192,8 +290,8 @@ Reload the window: Cmd+Shift+P, then **Developer: Reload Window**.
|
|
|
192
290
|
|
|
193
291
|
```toml
|
|
194
292
|
[mcp_servers.apple-photos]
|
|
195
|
-
command = "
|
|
196
|
-
args = ["
|
|
293
|
+
command = "npx"
|
|
294
|
+
args = ["-y", "@thenavidm/apple-photos-mcp-cli@latest"]
|
|
197
295
|
```
|
|
198
296
|
|
|
199
297
|
### Gemini CLI
|
|
@@ -202,7 +300,7 @@ args = ["--from", "git+https://github.com/navidmoazzez/apple-photos-mcp", "apple
|
|
|
202
300
|
|
|
203
301
|
### Everything else
|
|
204
302
|
|
|
205
|
-
Any stdio MCP client takes the same two things: the command `
|
|
303
|
+
Any stdio MCP client takes the same two things: the command `npx`, and the args above.
|
|
206
304
|
|
|
207
305
|
### claude.ai on the web, with a relay
|
|
208
306
|
|
|
@@ -215,7 +313,7 @@ That relay is not included here. It is a separate deployment with its own hostin
|
|
|
215
313
|
## 5. Check it worked 🩺
|
|
216
314
|
|
|
217
315
|
```bash
|
|
218
|
-
|
|
316
|
+
npx -y @thenavidm/apple-photos-mcp-cli@latest doctor
|
|
219
317
|
```
|
|
220
318
|
|
|
221
319
|
Or just ask your agent: **"run doctor on apple photos"**.
|
|
@@ -343,7 +441,7 @@ Run `doctor` before guessing. It names which of these it is.
|
|
|
343
441
|
|---|---|---|
|
|
344
442
|
| Permission denied reading the library | No Full Disk Access | [Section 3](#3-setup-). Quit the app with Cmd+Q, not just the window. |
|
|
345
443
|
| Server does not appear in the client | Bad JSON, usually a comma | Paste the config into a JSON validator. One bad comma hides every server. |
|
|
346
|
-
| `
|
|
444
|
+
| `npx: command not found` | The app cannot see your shell PATH | Use the absolute path from `which npx` |
|
|
347
445
|
| Write tools are missing | `APPLE_PHOTOS_READ_ONLY` is set | Unset it and restart the client |
|
|
348
446
|
| Reads work, writes fail | Photos automation not approved | Approve the popup, or Privacy & Security, then Automation |
|
|
349
447
|
| First search takes half a minute | Building the index | One time, about 25 seconds for 37,000 items. Cached afterwards. |
|
|
@@ -448,23 +546,22 @@ Delete the server from your client's config, or run `claude mcp remove apple-pho
|
|
|
448
546
|
|
|
449
547
|
## Questions
|
|
450
548
|
|
|
451
|
-
Run into a problem or have a question? [Open an issue](https://github.com/navidmoazzez/apple-photos-mcp/issues) and I will help.
|
|
549
|
+
Run into a problem or have a question? [Open an issue](https://github.com/navidmoazzez/apple-photos-mcp-cli/issues) and I will help.
|
|
452
550
|
|
|
453
|
-
## About the author
|
|
551
|
+
## About the author 👋
|
|
454
552
|
|
|
455
|
-
Navid Moazzez is a leading AI business strategist and the host of the AI Creator Summit, watched by 100,000+ creators. He helps creators and founders master AI and build their own AI Operating System (AI OS) to automate their business and life. This Apple Photos MCP server is one piece of that system.
|
|
553
|
+
Navid Moazzez is a leading AI business strategist, and the host of the AI Creator Summit, watched by 100,000+ creators. He helps creators and founders master AI and build their own AI Operating System (AI OS) to automate their business and life. This Apple Photos MCP server is one piece of that system.
|
|
456
554
|
|
|
457
555
|
**Links**
|
|
458
556
|
|
|
459
|
-
- Personal website: [navid.me](https://navid.me
|
|
460
|
-
- Link in bio: [navid.bio](https://navid.bio
|
|
461
|
-
- Navid Media: [navid.media](https://navid.media
|
|
557
|
+
- Personal website: [navid.me](https://navid.me)
|
|
558
|
+
- Link in bio: [navid.bio](https://navid.bio)
|
|
559
|
+
- Navid Media: [navid.media](https://navid.media)
|
|
462
560
|
- YouTube: [@thenavidm](https://youtube.com/@thenavidm?sub_confirmation=1) and [@thenavidai](https://youtube.com/@thenavidai?sub_confirmation=1)
|
|
463
561
|
- X: [@thenavidm](https://x.com/thenavidm)
|
|
464
562
|
- Instagram: [@thenavidm](https://instagram.com/thenavidm)
|
|
465
563
|
- LinkedIn: [thenavidm](https://linkedin.com/in/thenavidm)
|
|
466
564
|
|
|
467
|
-
If this is useful, star the repo and come say hi on [X](https://x.com/thenavidm).
|
|
468
565
|
|
|
469
566
|
## Dependencies
|
|
470
567
|
|
|
@@ -482,4 +579,4 @@ Not affiliated with, endorsed by, or connected to Apple Inc. Apple, macOS, Photo
|
|
|
482
579
|
|
|
483
580
|
---
|
|
484
581
|
|
|
485
|
-
© 2026 [NM Media](https://navid.media
|
|
582
|
+
© 2026 [NM Media](https://navid.media). Made with ❤️ by [Navid Moazzez](https://navid.me).
|
package/SKILL.md
CHANGED
|
@@ -16,6 +16,22 @@ description: >
|
|
|
16
16
|
The library is read directly from disk on this Mac. Nothing is uploaded, and
|
|
17
17
|
nothing here can permanently delete a photo.
|
|
18
18
|
|
|
19
|
+
|
|
20
|
+
## Two surfaces, same tools
|
|
21
|
+
|
|
22
|
+
The MCP server is for work inside a conversation. The CLI is for scripting,
|
|
23
|
+
piping and one-off questions, and costs no context until it is called.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
apple-photos-cli library-stats --json
|
|
27
|
+
apple-photos-cli search-photos "sunset" --limit 5 --screenshots exclude
|
|
28
|
+
apple-photos-cli photo-info --refs IMG_2073.MOV
|
|
29
|
+
apple-photos-cli <command> --help
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The command is the tool name with dashes. `--select total,videos` trims the
|
|
33
|
+
output, which matters on a library this size.
|
|
34
|
+
|
|
19
35
|
## The one thing that matters
|
|
20
36
|
|
|
21
37
|
Search returns candidates. It does not return answers.
|
package/lib/bridge.d.ts
CHANGED
|
@@ -14,30 +14,41 @@
|
|
|
14
14
|
* HQ connector can import so the hosted surface cannot drift from this one.
|
|
15
15
|
*
|
|
16
16
|
* Calls are proxied to the Python MCP server over stdio using the official
|
|
17
|
-
* client,
|
|
18
|
-
*
|
|
17
|
+
* client, so the Python needs no changes and there is one implementation of
|
|
18
|
+
* every tool.
|
|
19
19
|
*/
|
|
20
20
|
import type { Config } from "./config.js";
|
|
21
|
+
/** A content part as the MCP protocol carries it: text, image, or anything later. */
|
|
22
|
+
export type ContentPart = {
|
|
23
|
+
type: string;
|
|
24
|
+
text?: string;
|
|
25
|
+
data?: string;
|
|
26
|
+
mimeType?: string;
|
|
27
|
+
};
|
|
21
28
|
export type BridgeResult = {
|
|
29
|
+
/** Every part, untouched. Images must survive: look_at_photos returns them. */
|
|
30
|
+
content: ContentPart[];
|
|
31
|
+
/** The text parts joined, for the common case of a JSON payload. */
|
|
22
32
|
text: string;
|
|
23
33
|
isError: boolean;
|
|
24
34
|
};
|
|
25
|
-
/**
|
|
26
|
-
* One long-lived connection to the Python server.
|
|
27
|
-
*
|
|
28
|
-
* Spawning per call would pay `uv`'s dependency resolution and a Photos library
|
|
29
|
-
* scan every time, which is seconds each. The process is started on first use
|
|
30
|
-
* and reused.
|
|
31
|
-
*/
|
|
32
35
|
export declare class PythonBridge {
|
|
33
36
|
private readonly config;
|
|
34
37
|
private client?;
|
|
35
38
|
private starting?;
|
|
39
|
+
/** The tail of the engine's stderr, so a failure can say what actually went wrong. */
|
|
40
|
+
private stderrTail;
|
|
36
41
|
constructor(config: Config);
|
|
37
42
|
private connect;
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Call one Python tool and return every content part.
|
|
45
|
+
*
|
|
46
|
+
* `timeoutMs` matters: exporting originals pulls them out of iCloud first and
|
|
47
|
+
* rendering previews is not quick either, so the protocol default of 60s
|
|
48
|
+
* cancels work the engine is still doing.
|
|
49
|
+
*/
|
|
50
|
+
call(tool: string, args: Record<string, unknown>, timeoutMs?: number): Promise<BridgeResult>;
|
|
51
|
+
/** What the Python server says it offers, for checking the two lists agree. */
|
|
41
52
|
listTools(): Promise<string[]>;
|
|
42
53
|
close(): Promise<void>;
|
|
43
54
|
}
|
package/lib/bridge.js
CHANGED
|
@@ -14,23 +14,18 @@
|
|
|
14
14
|
* HQ connector can import so the hosted surface cannot drift from this one.
|
|
15
15
|
*
|
|
16
16
|
* Calls are proxied to the Python MCP server over stdio using the official
|
|
17
|
-
* client,
|
|
18
|
-
*
|
|
17
|
+
* client, so the Python needs no changes and there is one implementation of
|
|
18
|
+
* every tool.
|
|
19
19
|
*/
|
|
20
20
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
21
21
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
22
22
|
import { BridgeError } from "./errors.js";
|
|
23
|
-
/**
|
|
24
|
-
* One long-lived connection to the Python server.
|
|
25
|
-
*
|
|
26
|
-
* Spawning per call would pay `uv`'s dependency resolution and a Photos library
|
|
27
|
-
* scan every time, which is seconds each. The process is started on first use
|
|
28
|
-
* and reused.
|
|
29
|
-
*/
|
|
30
23
|
export class PythonBridge {
|
|
31
24
|
config;
|
|
32
25
|
client;
|
|
33
26
|
starting;
|
|
27
|
+
/** The tail of the engine's stderr, so a failure can say what actually went wrong. */
|
|
28
|
+
stderrTail = "";
|
|
34
29
|
constructor(config) {
|
|
35
30
|
this.config = config;
|
|
36
31
|
}
|
|
@@ -41,21 +36,38 @@ export class PythonBridge {
|
|
|
41
36
|
return this.starting;
|
|
42
37
|
this.starting = (async () => {
|
|
43
38
|
const client = new Client({ name: "apple-photos-cli", version: "1.0.0" }, { capabilities: {} });
|
|
44
|
-
// `uv run` resolves osxphotos and photoscript on first use and caches
|
|
45
|
-
// them, so the reader needs neither a virtualenv nor a pip install.
|
|
46
39
|
const transport = new StdioClientTransport({
|
|
47
40
|
command: this.config.pythonCommand,
|
|
48
41
|
args: this.config.pythonArgs,
|
|
49
42
|
env: { ...process.env, ...this.config.pythonEnv },
|
|
50
43
|
stderr: "pipe",
|
|
51
44
|
});
|
|
45
|
+
// Drain stderr immediately. The SDK pipes it into a PassThrough with a
|
|
46
|
+
// 16KB buffer; with no reader that fills, the OS pipe fills behind it,
|
|
47
|
+
// and the child blocks writing to stderr. osxphotos is chatty on a large
|
|
48
|
+
// library, so this is a real hang, not a theoretical one. Keeping the
|
|
49
|
+
// tail also means a failure can name the actual cause — a missing module,
|
|
50
|
+
// a Full Disk Access denial — instead of a boilerplate "install uv".
|
|
51
|
+
transport.stderr?.on("data", (chunk) => {
|
|
52
|
+
this.stderrTail = (this.stderrTail + chunk.toString()).slice(-4000);
|
|
53
|
+
});
|
|
54
|
+
// A dead client must not stay cached, or one engine crash bricks every
|
|
55
|
+
// later call for the life of the process with "Connection closed".
|
|
56
|
+
const forget = () => {
|
|
57
|
+
if (this.client === client)
|
|
58
|
+
this.client = undefined;
|
|
59
|
+
};
|
|
60
|
+
client.onclose = forget;
|
|
61
|
+
client.onerror = forget;
|
|
52
62
|
try {
|
|
53
|
-
|
|
63
|
+
// The default is 60s, and a cold `uv` run builds pyobjc, which takes
|
|
64
|
+
// longer than that on a fresh cache. Failing there reported "install
|
|
65
|
+
// uv" at the one moment uv was working correctly.
|
|
66
|
+
await client.connect(transport, { timeout: this.config.startupTimeoutMs });
|
|
54
67
|
}
|
|
55
68
|
catch (error) {
|
|
56
69
|
throw new BridgeError(`Could not start the Photos engine with \`${this.config.pythonCommand} ${this.config.pythonArgs.join(" ")}\`. ` +
|
|
57
|
-
`Install uv (https://docs.astral.sh/uv/) or set APPLE_PHOTOS_PYTHON to a Python that has osxphotos and photoscript.
|
|
58
|
-
`Underlying error: ${error.message}`);
|
|
70
|
+
`Install uv (https://docs.astral.sh/uv/) or set APPLE_PHOTOS_PYTHON to a Python that has osxphotos and photoscript.`, [error.message, this.stderrTail.trim()].filter(Boolean).join("\n"));
|
|
59
71
|
}
|
|
60
72
|
this.client = client;
|
|
61
73
|
return client;
|
|
@@ -67,25 +79,40 @@ export class PythonBridge {
|
|
|
67
79
|
this.starting = undefined;
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
|
-
/**
|
|
71
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Call one Python tool and return every content part.
|
|
84
|
+
*
|
|
85
|
+
* `timeoutMs` matters: exporting originals pulls them out of iCloud first and
|
|
86
|
+
* rendering previews is not quick either, so the protocol default of 60s
|
|
87
|
+
* cancels work the engine is still doing.
|
|
88
|
+
*/
|
|
89
|
+
async call(tool, args, timeoutMs) {
|
|
72
90
|
const client = await this.connect();
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
result = (await client.callTool({ name: tool, arguments: args }, undefined, { timeout: timeoutMs ?? this.config.requestTimeoutMs }));
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw new BridgeError(`The Photos engine failed while running ${tool}.`, [error.message, this.stderrTail.trim()].filter(Boolean).join("\n"));
|
|
97
|
+
}
|
|
98
|
+
const content = result.content ?? [];
|
|
99
|
+
const text = content
|
|
75
100
|
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
76
101
|
.map((part) => part.text)
|
|
77
102
|
.join("\n");
|
|
78
|
-
return { text, isError: result.isError === true };
|
|
103
|
+
return { content, text, isError: result.isError === true };
|
|
79
104
|
}
|
|
80
|
-
/** What the Python server says it offers
|
|
105
|
+
/** What the Python server says it offers, for checking the two lists agree. */
|
|
81
106
|
async listTools() {
|
|
82
107
|
const client = await this.connect();
|
|
83
108
|
const { tools } = await client.listTools();
|
|
84
109
|
return tools.map((tool) => tool.name);
|
|
85
110
|
}
|
|
86
111
|
async close() {
|
|
87
|
-
|
|
112
|
+
this.starting = undefined;
|
|
113
|
+
const client = this.client;
|
|
88
114
|
this.client = undefined;
|
|
115
|
+
await client?.close().catch(() => undefined);
|
|
89
116
|
}
|
|
90
117
|
}
|
|
91
118
|
//# sourceMappingURL=bridge.js.map
|
package/lib/bridge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src-ts/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAGjF,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src-ts/bridge.ts"],"names":[],"mappings":"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"}
|
package/lib/cli.d.ts
CHANGED
|
@@ -28,16 +28,7 @@ type Flag = {
|
|
|
28
28
|
help: string;
|
|
29
29
|
};
|
|
30
30
|
export declare function flagsFor(shape: ZodRawShape): Flag[];
|
|
31
|
-
/**
|
|
32
|
-
* Midjourney's own parameter spellings, accepted as flags.
|
|
33
|
-
*
|
|
34
|
-
* The schema names things in full because a tool description is read by a model
|
|
35
|
-
* that has never seen a Midjourney prompt. A person at a terminal has, and they
|
|
36
|
-
* will type `--ar 16:9`, because that is what the parameter is called
|
|
37
|
-
* everywhere Midjourney documents it. Refusing the name the whole ecosystem
|
|
38
|
-
* uses, to protect a naming convention nobody outside this repo can see, would
|
|
39
|
-
* be the wrong trade.
|
|
40
|
-
*/
|
|
31
|
+
/** Shorthands for this repo's arguments. Kept small on purpose. */
|
|
41
32
|
export declare const FLAG_ALIASES: Record<string, string>;
|
|
42
33
|
/**
|
|
43
34
|
* Parse argv against a tool's flags.
|
|
@@ -67,11 +58,12 @@ export declare function emitError(error: unknown): void;
|
|
|
67
58
|
*/
|
|
68
59
|
export declare function binName(): string;
|
|
69
60
|
/**
|
|
70
|
-
* Words people use mapped onto words
|
|
61
|
+
* Words people use mapped onto words these tools use.
|
|
71
62
|
*
|
|
72
|
-
* Without this, "
|
|
73
|
-
*
|
|
74
|
-
*
|
|
63
|
+
* Without this, "save my photos to disk" matches four browsing tools and never
|
|
64
|
+
* export-originals, because nothing in that phrase appears in its name or
|
|
65
|
+
* description. Every target below has to be vocabulary a tool actually
|
|
66
|
+
* contains; a test asserts it.
|
|
75
67
|
*/
|
|
76
68
|
export declare const SYNONYMS: Record<string, string[]>;
|
|
77
69
|
/**
|