@thenavidm/apple-photos-mcp-cli 1.0.0 → 1.0.2
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/lib/bridge.d.ts +43 -0
- package/lib/bridge.js +91 -0
- package/lib/bridge.js.map +1 -0
- package/lib/cli.d.ts +92 -0
- package/lib/cli.js +634 -0
- package/lib/cli.js.map +1 -0
- package/lib/config.d.ts +19 -0
- package/lib/config.js +58 -0
- package/lib/config.js.map +1 -0
- package/lib/errors.d.ts +15 -0
- package/lib/errors.js +22 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +117 -0
- package/lib/index.js.map +1 -0
- package/lib/safety.d.ts +27 -0
- package/lib/safety.js +70 -0
- package/lib/safety.js.map +1 -0
- package/lib/server.d.ts +10 -0
- package/lib/server.js +38 -0
- package/lib/server.js.map +1 -0
- package/lib/tools/index.d.ts +55 -0
- package/lib/tools/index.js +154 -0
- package/lib/tools/index.js.map +1 -0
- package/lib/tools/kit.d.ts +48 -0
- package/lib/tools/kit.js +64 -0
- package/lib/tools/kit.js.map +1 -0
- package/package.json +35 -10
- package/src/apple_photos_mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/__main__.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/config.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/doctor.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/library.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/previews.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/safety.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/search.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/server.cpython-313.pyc +0 -0
- package/src/apple_photos_mcp/__pycache__/writes.cpython-313.pyc +0 -0
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything read from the environment, in one place.
|
|
3
|
+
*
|
|
4
|
+
* There are no credentials here and there is nothing to configure to get
|
|
5
|
+
* started: the library is on this Mac and macOS decides who may read it. The
|
|
6
|
+
* only real setting is how to start Python.
|
|
7
|
+
*/
|
|
8
|
+
export type Config = {
|
|
9
|
+
pythonCommand: string;
|
|
10
|
+
pythonArgs: string[];
|
|
11
|
+
pythonEnv: Record<string, string>;
|
|
12
|
+
readOnly: boolean;
|
|
13
|
+
allowDestructive: boolean;
|
|
14
|
+
auditPath?: string;
|
|
15
|
+
exportDir?: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function loadConfig(): Config;
|
|
18
|
+
/** Every variable the code reads, so --help, the README and the tests agree. */
|
|
19
|
+
export declare const ENV_VARS: readonly ["APPLE_PHOTOS_PYTHON", "APPLE_PHOTOS_PYTHONPATH", "APPLE_PHOTOS_READ_ONLY", "APPLE_PHOTOS_ALLOW_DESTRUCTIVE", "APPLE_PHOTOS_AUDIT_LOG", "APPLE_PHOTOS_EXPORT_DIR"];
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything read from the environment, in one place.
|
|
3
|
+
*
|
|
4
|
+
* There are no credentials here and there is nothing to configure to get
|
|
5
|
+
* started: the library is on this Mac and macOS decides who may read it. The
|
|
6
|
+
* only real setting is how to start Python.
|
|
7
|
+
*/
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
function bool(name, fallback) {
|
|
10
|
+
const raw = process.env[name];
|
|
11
|
+
if (raw === undefined || raw.trim() === "")
|
|
12
|
+
return fallback;
|
|
13
|
+
return !["0", "false", "no", "off"].includes(raw.trim().toLowerCase());
|
|
14
|
+
}
|
|
15
|
+
function str(name) {
|
|
16
|
+
const raw = process.env[name];
|
|
17
|
+
return raw && raw.trim() ? raw.trim() : undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Where the Python engine lives, resolved from this file rather than the
|
|
21
|
+
* working directory.
|
|
22
|
+
*
|
|
23
|
+
* `src` relative to CWD works when you run from a clone and breaks everywhere
|
|
24
|
+
* else, which is every `npx` install: the engine ships inside the package and
|
|
25
|
+
* the caller is in their own project. dist/ sits one level under the package
|
|
26
|
+
* root, so the engine is always ../src from here.
|
|
27
|
+
*/
|
|
28
|
+
function packagedEnginePath() {
|
|
29
|
+
return fileURLToPath(new URL("../src", import.meta.url));
|
|
30
|
+
}
|
|
31
|
+
export function loadConfig() {
|
|
32
|
+
// `uv run --with ...` needs no virtualenv and caches after the first run, so
|
|
33
|
+
// a reader with only Node installed still gets a working server.
|
|
34
|
+
const explicit = str("APPLE_PHOTOS_PYTHON");
|
|
35
|
+
const pythonCommand = explicit ?? "uv";
|
|
36
|
+
const pythonArgs = explicit
|
|
37
|
+
? ["-m", "apple_photos_mcp"]
|
|
38
|
+
: ["run", "--with", "osxphotos", "--with", "photoscript", "--with", "mcp", "python3", "-m", "apple_photos_mcp"];
|
|
39
|
+
return {
|
|
40
|
+
pythonCommand,
|
|
41
|
+
pythonArgs,
|
|
42
|
+
pythonEnv: { PYTHONPATH: str("APPLE_PHOTOS_PYTHONPATH") ?? packagedEnginePath() },
|
|
43
|
+
readOnly: bool("APPLE_PHOTOS_READ_ONLY", false),
|
|
44
|
+
allowDestructive: bool("APPLE_PHOTOS_ALLOW_DESTRUCTIVE", true),
|
|
45
|
+
auditPath: str("APPLE_PHOTOS_AUDIT_LOG"),
|
|
46
|
+
exportDir: str("APPLE_PHOTOS_EXPORT_DIR"),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Every variable the code reads, so --help, the README and the tests agree. */
|
|
50
|
+
export const ENV_VARS = [
|
|
51
|
+
"APPLE_PHOTOS_PYTHON",
|
|
52
|
+
"APPLE_PHOTOS_PYTHONPATH",
|
|
53
|
+
"APPLE_PHOTOS_READ_ONLY",
|
|
54
|
+
"APPLE_PHOTOS_ALLOW_DESTRUCTIVE",
|
|
55
|
+
"APPLE_PHOTOS_AUDIT_LOG",
|
|
56
|
+
"APPLE_PHOTOS_EXPORT_DIR",
|
|
57
|
+
];
|
|
58
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src-ts/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAYzC,SAAS,IAAI,CAAC,IAAY,EAAE,QAAiB;IAC3C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAC;IAC5D,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AACzE,CAAC;AAED,SAAS,GAAG,CAAC,IAAY;IACvB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACpD,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,kBAAkB;IACzB,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,6EAA6E;IAC7E,iEAAiE;IACjE,MAAM,QAAQ,GAAG,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAC5C,MAAM,aAAa,GAAG,QAAQ,IAAI,IAAI,CAAC;IACvC,MAAM,UAAU,GAAG,QAAQ;QACzB,CAAC,CAAC,CAAC,IAAI,EAAE,kBAAkB,CAAC;QAC5B,CAAC,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,kBAAkB,CAAC,CAAC;IAElH,OAAO;QACL,aAAa;QACb,UAAU;QACV,SAAS,EAAE,EAAE,UAAU,EAAE,GAAG,CAAC,yBAAyB,CAAC,IAAI,kBAAkB,EAAE,EAAE;QACjF,QAAQ,EAAE,IAAI,CAAC,wBAAwB,EAAE,KAAK,CAAC;QAC/C,gBAAgB,EAAE,IAAI,CAAC,gCAAgC,EAAE,IAAI,CAAC;QAC9D,SAAS,EAAE,GAAG,CAAC,wBAAwB,CAAC;QACxC,SAAS,EAAE,GAAG,CAAC,yBAAyB,CAAC;KAC1C,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,qBAAqB;IACrB,yBAAyB;IACzB,wBAAwB;IACxB,gCAAgC;IAChC,wBAAwB;IACxB,yBAAyB;CACjB,CAAC"}
|
package/lib/errors.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Typed failures, each carrying the fix rather than just the symptom. */
|
|
2
|
+
export declare class PhotosError extends Error {
|
|
3
|
+
readonly detail: string;
|
|
4
|
+
constructor(message: string, detail?: string);
|
|
5
|
+
toJSON(): Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
/** The Python engine could not be started. Nothing reached the library. */
|
|
8
|
+
export declare class BridgeError extends PhotosError {
|
|
9
|
+
}
|
|
10
|
+
/** The engine ran but refused the request. */
|
|
11
|
+
export declare class ToolError extends PhotosError {
|
|
12
|
+
}
|
|
13
|
+
/** Writes are off, or a destructive tool was called without confirmation. */
|
|
14
|
+
export declare class WriteBlockedError extends PhotosError {
|
|
15
|
+
}
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Typed failures, each carrying the fix rather than just the symptom. */
|
|
2
|
+
export class PhotosError extends Error {
|
|
3
|
+
detail;
|
|
4
|
+
constructor(message, detail = "") {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = new.target.name;
|
|
7
|
+
this.detail = detail;
|
|
8
|
+
}
|
|
9
|
+
toJSON() {
|
|
10
|
+
return { error: this.message, type: this.name, ...(this.detail ? { detail: this.detail.slice(0, 500) } : {}) };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** The Python engine could not be started. Nothing reached the library. */
|
|
14
|
+
export class BridgeError extends PhotosError {
|
|
15
|
+
}
|
|
16
|
+
/** The engine ran but refused the request. */
|
|
17
|
+
export class ToolError extends PhotosError {
|
|
18
|
+
}
|
|
19
|
+
/** Writes are off, or a destructive tool was called without confirmation. */
|
|
20
|
+
export class WriteBlockedError extends PhotosError {
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src-ts/errors.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAE1E,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,MAAM,CAAS;IAExB,YAAY,OAAe,EAAE,MAAM,GAAG,EAAE;QACtC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,MAAM;QACJ,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IACjH,CAAC;CACF;AAED,2EAA2E;AAC3E,MAAM,OAAO,WAAY,SAAQ,WAAW;CAAG;AAE/C,8CAA8C;AAC9C,MAAM,OAAO,SAAU,SAAQ,WAAW;CAAG;AAE7C,6EAA6E;AAC7E,MAAM,OAAO,iBAAkB,SAAQ,WAAW;CAAG"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Entry point.
|
|
4
|
+
*
|
|
5
|
+
* `apple-photos-mcp` stdio, which is what MCP clients launch
|
|
6
|
+
* `apple-photos-cli <tool>` run one tool from the shell
|
|
7
|
+
* `apple-photos-cli doctor` check the setup and say what is wrong
|
|
8
|
+
*
|
|
9
|
+
* The shell surface is generated from the same `ALL_TOOLS` array the server
|
|
10
|
+
* registers, so every tool is a command and neither surface can drift.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Entry point.
|
|
4
|
+
*
|
|
5
|
+
* `apple-photos-mcp` stdio, which is what MCP clients launch
|
|
6
|
+
* `apple-photos-cli <tool>` run one tool from the shell
|
|
7
|
+
* `apple-photos-cli doctor` check the setup and say what is wrong
|
|
8
|
+
*
|
|
9
|
+
* The shell surface is generated from the same `ALL_TOOLS` array the server
|
|
10
|
+
* registers, so every tool is a command and neither surface can drift.
|
|
11
|
+
*/
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { loadConfig } from "./config.js";
|
|
14
|
+
import { buildServer, VERSION } from "./server.js";
|
|
15
|
+
import { isCliCommand, runCli } from "./cli.js";
|
|
16
|
+
const HELP = `apple-photos-mcp ${VERSION}
|
|
17
|
+
|
|
18
|
+
apple-photos-mcp Run over stdio. This is what an MCP client launches.
|
|
19
|
+
apple-photos-cli tools List every tool as a shell command.
|
|
20
|
+
apple-photos-cli <tool> [flags] Run one tool. Same names as the MCP surface.
|
|
21
|
+
apple-photos-cli <tool> --help What that tool takes.
|
|
22
|
+
apple-photos-cli doctor Check the setup and report what is wrong.
|
|
23
|
+
apple-photos-mcp --version Print the version.
|
|
24
|
+
|
|
25
|
+
Every command prints JSON on --json, trims it with --select, and reports
|
|
26
|
+
errors as JSON on stderr.
|
|
27
|
+
|
|
28
|
+
There are no credentials. The library is on this Mac and macOS decides who may
|
|
29
|
+
read it; the first run will ask for permission. Nothing is uploaded anywhere.
|
|
30
|
+
|
|
31
|
+
Engine:
|
|
32
|
+
APPLE_PHOTOS_PYTHON a Python with osxphotos and photoscript.
|
|
33
|
+
Defaults to uv, which fetches them on demand.
|
|
34
|
+
APPLE_PHOTOS_PYTHONPATH where the engine package lives, default src
|
|
35
|
+
|
|
36
|
+
Behaviour:
|
|
37
|
+
APPLE_PHOTOS_EXPORT_DIR where export_originals writes by default
|
|
38
|
+
|
|
39
|
+
Safety:
|
|
40
|
+
APPLE_PHOTOS_READ_ONLY=1 hide everything that is not a read
|
|
41
|
+
APPLE_PHOTOS_ALLOW_DESTRUCTIVE=0 keep reads and edits, block archiving
|
|
42
|
+
APPLE_PHOTOS_AUDIT_LOG append-only log of every attempted change
|
|
43
|
+
|
|
44
|
+
https://github.com/navidmoazzez/apple-photos-mcp
|
|
45
|
+
`;
|
|
46
|
+
function invokedAsCli() {
|
|
47
|
+
const name = (process.argv[1] ?? "").split("/").pop() ?? "";
|
|
48
|
+
return name.startsWith("apple-photos-cli");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* This only runs on a Mac, but the package must still install anywhere.
|
|
52
|
+
*
|
|
53
|
+
* An `"os": ["darwin"]` field in package.json looks right and breaks the thing
|
|
54
|
+
* that matters: the HQ connector imports ALL_TOOLS for its schemas on a Linux
|
|
55
|
+
* builder, never executing a tool, and npm refuses to install at all. The
|
|
56
|
+
* pyproject next door already made this call for the same reason — its comment
|
|
57
|
+
* notes that platform markers make `pip install` fail on Linux "instead of
|
|
58
|
+
* installing and then telling the user this server needs a Mac".
|
|
59
|
+
*
|
|
60
|
+
* So: install anywhere, refuse to run anywhere but macOS, and say why.
|
|
61
|
+
*/
|
|
62
|
+
function requireMac() {
|
|
63
|
+
if (process.platform === "darwin")
|
|
64
|
+
return;
|
|
65
|
+
process.stderr.write(`${JSON.stringify({
|
|
66
|
+
error: "Apple Photos only exists on macOS, so this server cannot run on " +
|
|
67
|
+
`${process.platform}. It installs anywhere because the tool definitions are ` +
|
|
68
|
+
"read by other tooling, but reaching a library needs a Mac.",
|
|
69
|
+
}, null, 2)}\n`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
async function main() {
|
|
73
|
+
const argv = process.argv.slice(2);
|
|
74
|
+
const command = argv[0];
|
|
75
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
76
|
+
process.stdout.write(`${VERSION}\n`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
requireMac();
|
|
80
|
+
if (invokedAsCli() && argv.length === 0) {
|
|
81
|
+
process.exitCode = await runCli(["tools"]);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (isCliCommand(argv)) {
|
|
85
|
+
process.exitCode = await runCli(argv);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Neither binary takes a positional argument that is not a command, so a
|
|
89
|
+
// stray word is a typo rather than a reason to sit waiting on stdin.
|
|
90
|
+
if (command !== undefined && !command.startsWith("-") && command !== "help") {
|
|
91
|
+
process.stderr.write(`${JSON.stringify({ error: `Unknown command '${command}'. Run \`apple-photos-cli\` to list them.` }, null, 2)}\n`);
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (argv.includes("--help") || argv.includes("-h") || command === "help") {
|
|
96
|
+
process.stdout.write(HELP);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
100
|
+
process.stdout.write(`${VERSION}\n`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const built = buildServer(loadConfig());
|
|
104
|
+
const transport = new StdioServerTransport();
|
|
105
|
+
await built.server.connect(transport);
|
|
106
|
+
const shutdown = async () => {
|
|
107
|
+
await built.bridge.close();
|
|
108
|
+
process.exit(0);
|
|
109
|
+
};
|
|
110
|
+
process.on("SIGTERM", () => void shutdown());
|
|
111
|
+
process.on("SIGINT", () => void shutdown());
|
|
112
|
+
}
|
|
113
|
+
main().catch((error) => {
|
|
114
|
+
process.stderr.write(`[apple-photos-mcp] ${error.message}\n`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src-ts/index.ts"],"names":[],"mappings":"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"}
|
package/lib/safety.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether a change is allowed to reach the library.
|
|
3
|
+
*
|
|
4
|
+
* The hazard here is not publishing, it is losing something. Nothing in a
|
|
5
|
+
* Photos library is public, but a wrong `archive_photos` moves someone's own
|
|
6
|
+
* pictures out of view, and macOS gives this process no way to put them back.
|
|
7
|
+
*
|
|
8
|
+
* So archiving asks. Favouriting, titling, keywording and adding to an album do
|
|
9
|
+
* not: each is one click to undo in Photos, and confirming everything trains
|
|
10
|
+
* the reflex that makes the confirmation on a real removal worthless.
|
|
11
|
+
*/
|
|
12
|
+
import type { Config } from "./config.js";
|
|
13
|
+
export type Risk = "read" | "write" | "destructive";
|
|
14
|
+
export type Surface = "mcp" | "cli";
|
|
15
|
+
export declare function needsConfirm(risk: Risk): boolean;
|
|
16
|
+
export declare class WriteGuard {
|
|
17
|
+
private readonly config;
|
|
18
|
+
private readonly surface;
|
|
19
|
+
constructor(config: Config, surface?: Surface);
|
|
20
|
+
private get confirmFlag();
|
|
21
|
+
get readOnly(): boolean;
|
|
22
|
+
check(tool: string, risk: Risk, confirm: boolean | undefined, summary: string): void;
|
|
23
|
+
private audit;
|
|
24
|
+
}
|
|
25
|
+
export declare function annotationsFor(risk: Risk, options?: {
|
|
26
|
+
idempotent?: boolean;
|
|
27
|
+
}): Record<string, boolean>;
|
package/lib/safety.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether a change is allowed to reach the library.
|
|
3
|
+
*
|
|
4
|
+
* The hazard here is not publishing, it is losing something. Nothing in a
|
|
5
|
+
* Photos library is public, but a wrong `archive_photos` moves someone's own
|
|
6
|
+
* pictures out of view, and macOS gives this process no way to put them back.
|
|
7
|
+
*
|
|
8
|
+
* So archiving asks. Favouriting, titling, keywording and adding to an album do
|
|
9
|
+
* not: each is one click to undo in Photos, and confirming everything trains
|
|
10
|
+
* the reflex that makes the confirmation on a real removal worthless.
|
|
11
|
+
*/
|
|
12
|
+
import { appendFileSync } from "node:fs";
|
|
13
|
+
import { WriteBlockedError } from "./errors.js";
|
|
14
|
+
export function needsConfirm(risk) {
|
|
15
|
+
return risk === "destructive";
|
|
16
|
+
}
|
|
17
|
+
export class WriteGuard {
|
|
18
|
+
config;
|
|
19
|
+
surface;
|
|
20
|
+
constructor(config, surface = "mcp") {
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.surface = surface;
|
|
23
|
+
}
|
|
24
|
+
get confirmFlag() {
|
|
25
|
+
return this.surface === "cli" ? "--confirm" : "confirm: true";
|
|
26
|
+
}
|
|
27
|
+
get readOnly() {
|
|
28
|
+
return this.config.readOnly;
|
|
29
|
+
}
|
|
30
|
+
check(tool, risk, confirm, summary) {
|
|
31
|
+
if (risk === "read")
|
|
32
|
+
return;
|
|
33
|
+
if (this.config.readOnly) {
|
|
34
|
+
this.audit(tool, risk, summary, "blocked: read-only");
|
|
35
|
+
throw new WriteBlockedError(`${tool} is unavailable: this server is running with APPLE_PHOTOS_READ_ONLY=1.`);
|
|
36
|
+
}
|
|
37
|
+
if (needsConfirm(risk)) {
|
|
38
|
+
if (!this.config.allowDestructive) {
|
|
39
|
+
this.audit(tool, risk, summary, "blocked: destructive disabled");
|
|
40
|
+
throw new WriteBlockedError(`${tool} is unavailable: this server is running with APPLE_PHOTOS_ALLOW_DESTRUCTIVE=0.`);
|
|
41
|
+
}
|
|
42
|
+
if (confirm !== true) {
|
|
43
|
+
this.audit(tool, risk, summary, "blocked: no confirm");
|
|
44
|
+
throw new WriteBlockedError(`${tool} cannot be undone from here, so it will not run without ${this.confirmFlag}. About to: ${summary}.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
this.audit(tool, risk, summary, "allowed");
|
|
48
|
+
}
|
|
49
|
+
audit(tool, risk, summary, outcome) {
|
|
50
|
+
if (!this.config.auditPath)
|
|
51
|
+
return;
|
|
52
|
+
const line = JSON.stringify({ at: new Date().toISOString(), surface: this.surface, tool, risk, summary, outcome });
|
|
53
|
+
try {
|
|
54
|
+
appendFileSync(this.config.auditPath, `${line}\n`, { mode: 0o600 });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// A failing audit log must never take the tool call down with it.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function annotationsFor(risk, options = {}) {
|
|
62
|
+
return {
|
|
63
|
+
readOnlyHint: risk === "read",
|
|
64
|
+
destructiveHint: risk === "destructive",
|
|
65
|
+
idempotentHint: options.idempotent ?? risk === "read",
|
|
66
|
+
// Everything stays on this Mac: no network, no upload, nothing leaves.
|
|
67
|
+
openWorldHint: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safety.js","sourceRoot":"","sources":["../src-ts/safety.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAKhD,MAAM,UAAU,YAAY,CAAC,IAAU;IACrC,OAAO,IAAI,KAAK,aAAa,CAAC;AAChC,CAAC;AAED,MAAM,OAAO,UAAU;IACJ,MAAM,CAAS;IACf,OAAO,CAAU;IAElC,YAAY,MAAc,EAAE,UAAmB,KAAK;QAClD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,IAAY,WAAW;QACrB,OAAO,IAAI,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,eAAe,CAAC;IAChE,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,IAAY,EAAE,IAAU,EAAE,OAA4B,EAAE,OAAe;QAC3E,IAAI,IAAI,KAAK,MAAM;YAAE,OAAO;QAE5B,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YACzB,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAC;YACtD,MAAM,IAAI,iBAAiB,CAAC,GAAG,IAAI,wEAAwE,CAAC,CAAC;QAC/G,CAAC;QAED,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;YACvB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,gBAAgB,EAAE,CAAC;gBAClC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,+BAA+B,CAAC,CAAC;gBACjE,MAAM,IAAI,iBAAiB,CAAC,GAAG,IAAI,gFAAgF,CAAC,CAAC;YACvH,CAAC;YACD,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,qBAAqB,CAAC,CAAC;gBACvD,MAAM,IAAI,iBAAiB,CACzB,GAAG,IAAI,2DAA2D,IAAI,CAAC,WAAW,eAAe,OAAO,GAAG,CAC5G,CAAC;YACJ,CAAC;QACH,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IAC7C,CAAC;IAEO,KAAK,CAAC,IAAY,EAAE,IAAU,EAAE,OAAe,EAAE,OAAe;QACtE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS;YAAE,OAAO;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QACnH,IAAI,CAAC;YACH,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,IAAI,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,kEAAkE;QACpE,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,cAAc,CAAC,IAAU,EAAE,UAAoC,EAAE;IAC/E,OAAO;QACL,YAAY,EAAE,IAAI,KAAK,MAAM;QAC7B,eAAe,EAAE,IAAI,KAAK,aAAa;QACvC,cAAc,EAAE,OAAO,CAAC,UAAU,IAAI,IAAI,KAAK,MAAM;QACrD,uEAAuE;QACvE,aAAa,EAAE,KAAK;KACrB,CAAC;AACJ,CAAC"}
|
package/lib/server.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Assembles the MCP server: instructions, tools, and the read-only filter. */
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { PythonBridge } from "./bridge.js";
|
|
4
|
+
import type { Config } from "./config.js";
|
|
5
|
+
export declare const VERSION: string;
|
|
6
|
+
export type BuiltServer = {
|
|
7
|
+
server: McpServer;
|
|
8
|
+
bridge: PythonBridge;
|
|
9
|
+
};
|
|
10
|
+
export declare function buildServer(config: Config): BuiltServer;
|
package/lib/server.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Assembles the MCP server: instructions, tools, and the read-only filter. */
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { PythonBridge } from "./bridge.js";
|
|
5
|
+
import { WriteGuard } from "./safety.js";
|
|
6
|
+
import { ALL_TOOLS } from "./tools/index.js";
|
|
7
|
+
import { register } from "./tools/kit.js";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const pkg = require("../package.json");
|
|
10
|
+
export const VERSION = pkg.version;
|
|
11
|
+
const INSTRUCTIONS = `Tools for the user's own Apple Photos library, read directly on this Mac. Nothing is uploaded anywhere.
|
|
12
|
+
|
|
13
|
+
How to actually find something:
|
|
14
|
+
|
|
15
|
+
1. search_photos first. It queries Apple's own on-device index across the whole library, so the match count is library-wide; \`limit\` caps what comes back, not what is searched.
|
|
16
|
+
2. Then look_at_photos on the top few. Search returns candidates, not answers, and filenames tell you nothing. Look before you describe, recommend or choose.
|
|
17
|
+
3. Only then reply, naming the file and the date so the user can find it.
|
|
18
|
+
|
|
19
|
+
Three things that prevent confident wrong answers:
|
|
20
|
+
|
|
21
|
+
- For anything about scale or proportion, call library_stats. It gives real totals in one call. Counting by running searches and reading results double-counts overlapping terms and cannot see the items that carry no place or label, which in a typical library is thousands.
|
|
22
|
+
- Apple's visual vocabulary is closed, about 1,500 words. If a result carries \`unmatched_terms\`, Apple has never heard of that word and rephrasing the same idea will not help. Read \`did_you_mean\`, or call list_vocabulary.
|
|
23
|
+
- Screenshots are often a fifth of a library and skew every count. Use \`screenshots: "exclude"\` when the question is about photographs.
|
|
24
|
+
|
|
25
|
+
Nothing here can delete a photo; macOS does not permit it. archive_photos moves items into an album for the user to empty by hand, and it is the one tool that asks for confirmation.`;
|
|
26
|
+
export function buildServer(config) {
|
|
27
|
+
const server = new McpServer({ name: "apple-photos-mcp", version: VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
|
|
28
|
+
const bridge = new PythonBridge(config);
|
|
29
|
+
const guard = new WriteGuard(config, "mcp");
|
|
30
|
+
const context = { bridge, config, guard };
|
|
31
|
+
// READ_ONLY removes writes from the list rather than failing them when
|
|
32
|
+
// called. A model cannot call a tool it cannot see.
|
|
33
|
+
const tools = ALL_TOOLS.filter((tool) => !config.readOnly || tool.risk === "read");
|
|
34
|
+
for (const tool of tools)
|
|
35
|
+
register(server, () => context, tool);
|
|
36
|
+
return { server, bridge };
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src-ts/server.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAoB,MAAM,gBAAgB,CAAC;AAE5D,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,GAAG,GAAG,OAAO,CAAC,iBAAiB,CAAwB,CAAC;AAC9D,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;AAEnC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;sLAciK,CAAC;AAIvL,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,EAC9C,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,CAC5D,CAAC;IAEF,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAEvD,uEAAuE;IACvE,oDAAoD;IACpD,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;IACnF,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,QAAQ,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAEhE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every tool, mirroring the Python engine exactly.
|
|
3
|
+
*
|
|
4
|
+
* The names and arguments match `src/apple_photos_mcp/server.py` one for one,
|
|
5
|
+
* because this layer proxies rather than reimplements. A test asserts the two
|
|
6
|
+
* lists agree, so a tool added to Python and forgotten here fails the suite
|
|
7
|
+
* rather than quietly going missing from the CLI and from HQ.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { type AnyToolSpec } from "./kit.js";
|
|
11
|
+
export declare const readTools: (import("./kit.js").ToolSpec<{
|
|
12
|
+
query: z.ZodOptional<z.ZodString>;
|
|
13
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
kind: z.ZodOptional<z.ZodEnum<["photo", "video"]>>;
|
|
15
|
+
person: z.ZodOptional<z.ZodString>;
|
|
16
|
+
album: z.ZodOptional<z.ZodString>;
|
|
17
|
+
place: z.ZodOptional<z.ZodString>;
|
|
18
|
+
year: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
date_from: z.ZodOptional<z.ZodString>;
|
|
20
|
+
date_to: z.ZodOptional<z.ZodString>;
|
|
21
|
+
favorites_only: z.ZodOptional<z.ZodBoolean>;
|
|
22
|
+
screenshots: z.ZodOptional<z.ZodEnum<["include", "exclude", "only"]>>;
|
|
23
|
+
include_hidden: z.ZodOptional<z.ZodBoolean>;
|
|
24
|
+
}> | import("./kit.js").ToolSpec<{
|
|
25
|
+
refs: z.ZodArray<z.ZodString, "many">;
|
|
26
|
+
size: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
}> | import("./kit.js").ToolSpec<{
|
|
28
|
+
refs: z.ZodArray<z.ZodString, "many">;
|
|
29
|
+
}> | import("./kit.js").ToolSpec<{}> | import("./kit.js").ToolSpec<{
|
|
30
|
+
starts_with: z.ZodOptional<z.ZodString>;
|
|
31
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
32
|
+
}>)[];
|
|
33
|
+
export declare const writeTools: (import("./kit.js").ToolSpec<{
|
|
34
|
+
refs: z.ZodArray<z.ZodString, "many">;
|
|
35
|
+
directory: z.ZodOptional<z.ZodString>;
|
|
36
|
+
}> | import("./kit.js").ToolSpec<{
|
|
37
|
+
refs: z.ZodArray<z.ZodString, "many">;
|
|
38
|
+
favorite: z.ZodOptional<z.ZodBoolean>;
|
|
39
|
+
}> | import("./kit.js").ToolSpec<{
|
|
40
|
+
ref: z.ZodString;
|
|
41
|
+
title: z.ZodString;
|
|
42
|
+
}> | import("./kit.js").ToolSpec<{
|
|
43
|
+
ref: z.ZodString;
|
|
44
|
+
description: z.ZodString;
|
|
45
|
+
}> | import("./kit.js").ToolSpec<{
|
|
46
|
+
refs: z.ZodArray<z.ZodString, "many">;
|
|
47
|
+
keywords: z.ZodArray<z.ZodString, "many">;
|
|
48
|
+
}> | import("./kit.js").ToolSpec<{
|
|
49
|
+
album: z.ZodString;
|
|
50
|
+
refs: z.ZodArray<z.ZodString, "many">;
|
|
51
|
+
}> | import("./kit.js").ToolSpec<{
|
|
52
|
+
confirm: z.ZodOptional<z.ZodBoolean>;
|
|
53
|
+
refs: z.ZodArray<z.ZodString, "many">;
|
|
54
|
+
}>)[];
|
|
55
|
+
export declare const ALL_TOOLS: AnyToolSpec[];
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every tool, mirroring the Python engine exactly.
|
|
3
|
+
*
|
|
4
|
+
* The names and arguments match `src/apple_photos_mcp/server.py` one for one,
|
|
5
|
+
* because this layer proxies rather than reimplements. A test asserts the two
|
|
6
|
+
* lists agree, so a tool added to Python and forgotten here fails the suite
|
|
7
|
+
* rather than quietly going missing from the CLI and from HQ.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { confirmArg, defineTool } from "./kit.js";
|
|
11
|
+
const REFS = z
|
|
12
|
+
.array(z.string())
|
|
13
|
+
.describe("Item refs: Photos uuids from search_photos, or exact filenames like IMG_8402.mov.");
|
|
14
|
+
export const readTools = [
|
|
15
|
+
defineTool({
|
|
16
|
+
name: "search_photos",
|
|
17
|
+
title: "Search the library",
|
|
18
|
+
description: "Search the entire Photos library through Apple's own on-device index: what a picture looks like, text read inside it, the place, the activity, and any named faces.\n\nThis covers every item, not a sample. `limit` caps what comes back, not what is searched, so the match count is library-wide.\n\nApple's visual vocabulary is a closed list of roughly 1,500 words. A term it has never heard of matches nothing, and no rephrasing of the same idea helps: check `unmatched_terms` in the result, or call list_vocabulary.\n\nSearch returns candidates, not answers. The filenames tell you nothing, so call look_at_photos on the top few before describing them.",
|
|
19
|
+
schema: {
|
|
20
|
+
query: z.string().optional().describe("What to look for, in Apple's vocabulary. Empty returns the newest items."),
|
|
21
|
+
limit: z.number().int().min(1).max(200).optional().describe("How many to return. Defaults to 12."),
|
|
22
|
+
kind: z.enum(["photo", "video"]).optional().describe("Restrict to photos or videos."),
|
|
23
|
+
person: z.string().optional().describe("A face the user has named in Photos."),
|
|
24
|
+
album: z.string().optional().describe("Restrict to one album."),
|
|
25
|
+
place: z.string().optional().describe("A place name, as Photos records it."),
|
|
26
|
+
year: z.number().int().optional().describe("Restrict to one year."),
|
|
27
|
+
date_from: z.string().optional().describe("ISO date, inclusive."),
|
|
28
|
+
date_to: z.string().optional().describe("ISO date, inclusive."),
|
|
29
|
+
favorites_only: z.boolean().optional().describe("Only items the user favourited."),
|
|
30
|
+
screenshots: z
|
|
31
|
+
.enum(["include", "exclude", "only"])
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Screenshots are a fifth of a typical library and skew any count. Defaults to include."),
|
|
34
|
+
include_hidden: z.boolean().optional().describe("Include hidden items."),
|
|
35
|
+
},
|
|
36
|
+
risk: "read",
|
|
37
|
+
}),
|
|
38
|
+
defineTool({
|
|
39
|
+
name: "look_at_photos",
|
|
40
|
+
title: "Actually look at photos",
|
|
41
|
+
description: "Return the images themselves so they can be looked at, rather than reasoned about from filenames and labels.\n\nCall this on the top few results before describing, recommending or choosing between them. Search gives candidates; this is the only way to know what is in them.",
|
|
42
|
+
schema: {
|
|
43
|
+
refs: REFS,
|
|
44
|
+
size: z.number().int().min(64).max(2048).optional().describe("Longest edge in pixels. Defaults to 640."),
|
|
45
|
+
},
|
|
46
|
+
risk: "read",
|
|
47
|
+
}),
|
|
48
|
+
defineTool({
|
|
49
|
+
name: "photo_info",
|
|
50
|
+
title: "Full metadata for specific items",
|
|
51
|
+
description: "Everything Photos knows about specific items: date, place, camera and lens, dimensions, albums, keywords, faces, and whether the original is on this Mac or still in iCloud.",
|
|
52
|
+
schema: { refs: REFS },
|
|
53
|
+
risk: "read",
|
|
54
|
+
}),
|
|
55
|
+
defineTool({
|
|
56
|
+
name: "library_stats",
|
|
57
|
+
title: "Whole-library totals",
|
|
58
|
+
description: "Real totals for the whole library in one call: items, photos against videos, favourites, screenshots, how many carry ML labels, text or a place, named people, and every album.\n\nUse this for any question about scale or proportion. Counting by running searches and eyeballing results both costs far more and gets the wrong answer, because it double-counts overlapping terms and cannot see items with no place or label at all.",
|
|
59
|
+
schema: {},
|
|
60
|
+
risk: "read",
|
|
61
|
+
}),
|
|
62
|
+
defineTool({
|
|
63
|
+
name: "list_vocabulary",
|
|
64
|
+
title: "What Apple can actually search for",
|
|
65
|
+
description: "List the scene labels Apple's index understands. The vocabulary is closed, about 1,500 words, so this is how to find the term that will match rather than guessing synonyms that cannot.",
|
|
66
|
+
schema: {
|
|
67
|
+
starts_with: z.string().optional().describe("Filter to labels starting with this."),
|
|
68
|
+
limit: z.number().int().min(1).max(2000).optional().describe("How many to return. Defaults to 200."),
|
|
69
|
+
},
|
|
70
|
+
risk: "read",
|
|
71
|
+
}),
|
|
72
|
+
defineTool({
|
|
73
|
+
name: "doctor",
|
|
74
|
+
title: "Check the setup",
|
|
75
|
+
description: "Check that the Photos library is reachable, the engine starts, and this Mac has granted the permissions needed. Run it first when something fails, because a permissions problem and an empty library look identical from a tool call.",
|
|
76
|
+
schema: {},
|
|
77
|
+
risk: "read",
|
|
78
|
+
}),
|
|
79
|
+
];
|
|
80
|
+
export const writeTools = [
|
|
81
|
+
defineTool({
|
|
82
|
+
name: "export_originals",
|
|
83
|
+
title: "Export originals to disk",
|
|
84
|
+
description: "Export the original files to a folder on this Mac. Most items live in iCloud rather than on the disk, so an export downloads them first and can be slow.\n\nNothing is uploaded anywhere: the files land in a local folder and stay there.",
|
|
85
|
+
schema: {
|
|
86
|
+
refs: REFS,
|
|
87
|
+
directory: z.string().optional().describe("Where to write. Defaults to a dated folder under ~/Downloads."),
|
|
88
|
+
},
|
|
89
|
+
risk: "write",
|
|
90
|
+
idempotent: true,
|
|
91
|
+
summary: (args) => `export ${args.refs.length} original(s)`,
|
|
92
|
+
}),
|
|
93
|
+
defineTool({
|
|
94
|
+
name: "favorite_photos",
|
|
95
|
+
title: "Favourite or unfavourite",
|
|
96
|
+
description: "Mark items as favourites, or clear the mark. One click to undo in Photos, so this does not ask for confirmation.",
|
|
97
|
+
schema: {
|
|
98
|
+
refs: REFS,
|
|
99
|
+
favorite: z.boolean().optional().describe("False to unfavourite. Defaults to true."),
|
|
100
|
+
},
|
|
101
|
+
risk: "write",
|
|
102
|
+
idempotent: true,
|
|
103
|
+
summary: (args) => `${args.favorite === false ? "unfavourite" : "favourite"} ${args.refs.length} item(s)`,
|
|
104
|
+
}),
|
|
105
|
+
defineTool({
|
|
106
|
+
name: "set_photo_title",
|
|
107
|
+
title: "Set a title",
|
|
108
|
+
description: "Set the title on one item, as shown in Photos.",
|
|
109
|
+
schema: { ref: z.string().describe("One uuid or filename."), title: z.string().describe("The title to set.") },
|
|
110
|
+
risk: "write",
|
|
111
|
+
idempotent: true,
|
|
112
|
+
summary: (args) => `title ${String(args.ref)}`,
|
|
113
|
+
}),
|
|
114
|
+
defineTool({
|
|
115
|
+
name: "set_photo_description",
|
|
116
|
+
title: "Set a description",
|
|
117
|
+
description: "Set the description, the caption field Photos shows under an item.",
|
|
118
|
+
schema: {
|
|
119
|
+
ref: z.string().describe("One uuid or filename."),
|
|
120
|
+
description: z.string().describe("The description to set."),
|
|
121
|
+
},
|
|
122
|
+
risk: "write",
|
|
123
|
+
idempotent: true,
|
|
124
|
+
summary: (args) => `describe ${String(args.ref)}`,
|
|
125
|
+
}),
|
|
126
|
+
defineTool({
|
|
127
|
+
name: "add_keywords",
|
|
128
|
+
title: "Add keywords",
|
|
129
|
+
description: "Add keywords to items. Keywords are how a library stays searchable beyond what Apple's own index recognises.",
|
|
130
|
+
schema: { refs: REFS, keywords: z.array(z.string()).describe("Keywords to add.") },
|
|
131
|
+
risk: "write",
|
|
132
|
+
idempotent: true,
|
|
133
|
+
summary: (args) => `keyword ${args.refs.length} item(s)`,
|
|
134
|
+
}),
|
|
135
|
+
defineTool({
|
|
136
|
+
name: "add_to_album",
|
|
137
|
+
title: "Add to an album",
|
|
138
|
+
description: "Add items to an album, creating it if it does not exist.",
|
|
139
|
+
schema: { album: z.string().describe("Album name."), refs: REFS },
|
|
140
|
+
risk: "write",
|
|
141
|
+
idempotent: true,
|
|
142
|
+
summary: (args) => `add ${args.refs.length} item(s) to '${String(args.album)}'`,
|
|
143
|
+
}),
|
|
144
|
+
defineTool({
|
|
145
|
+
name: "archive_photos",
|
|
146
|
+
title: "Move items into an archive album",
|
|
147
|
+
description: "Move items into an album for the user to review and empty by hand.\n\nThis is as close to deleting as anything here gets: macOS does not permit a process to delete from a Photos library, so nothing is destroyed, but the items leave the flow the user was looking at and only they can put them back. It needs confirmation for that reason.",
|
|
148
|
+
schema: { refs: REFS, ...confirmArg },
|
|
149
|
+
risk: "destructive",
|
|
150
|
+
summary: (args) => `archive ${args.refs.length} item(s)`,
|
|
151
|
+
}),
|
|
152
|
+
];
|
|
153
|
+
export const ALL_TOOLS = [...readTools, ...writeTools];
|
|
154
|
+
//# sourceMappingURL=index.js.map
|