findmy-cli 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.
@@ -0,0 +1,36 @@
1
+ {
2
+ "id": "findmy-cli",
3
+ "name": "Find My",
4
+ "description": "macOS-only. Query Find My friend locations by driving FindMy.app via screen capture and Vision OCR. Returns name, coarse location (city, state), staleness, and distance. Shells out to the `findmy` binary (install via `brew install omarshahine/tap/findmy-cli`). Requires Screen Recording permission granted to the host process.",
5
+ "version": "0.1.0",
6
+ "platforms": [
7
+ "darwin"
8
+ ],
9
+ "requires": {
10
+ "binaries": [
11
+ "findmy",
12
+ "findmy-helper"
13
+ ],
14
+ "osPermissions": [
15
+ "macos:screen-recording"
16
+ ]
17
+ },
18
+ "configSchema": {
19
+ "type": "object",
20
+ "properties": {
21
+ "cliPath": {
22
+ "type": "string",
23
+ "description": "Path to the findmy binary. Defaults to PATH lookup.",
24
+ "default": "findmy"
25
+ }
26
+ },
27
+ "additionalProperties": false
28
+ },
29
+ "uiHints": {
30
+ "cliPath": {
31
+ "label": "CLI Path",
32
+ "help": "Path or command name for the findmy binary. Defaults to finding it on PATH (typically /opt/homebrew/bin/findmy after `brew install omarshahine/tap/findmy-cli`).",
33
+ "placeholder": "findmy"
34
+ }
35
+ }
36
+ }
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "findmy-cli",
3
+ "version": "0.1.0",
4
+ "description": "OpenClaw plugin for macOS Find My friend locations. Shells out to the findmy CLI to drive FindMy.app via screen capture and Vision OCR. macOS-only. Install findmy first via `brew install omarshahine/tap/findmy-cli`.",
5
+ "type": "module",
6
+ "openclaw": {
7
+ "extensions": [
8
+ "./src/index.ts"
9
+ ],
10
+ "compat": {
11
+ "pluginApi": ">=2026.3.23"
12
+ },
13
+ "build": {
14
+ "openclawVersion": "2026.4.15"
15
+ },
16
+ "environment": {
17
+ "binaries": [
18
+ "findmy",
19
+ "findmy-helper"
20
+ ],
21
+ "osPermissions": [
22
+ "macos:screen-recording"
23
+ ]
24
+ },
25
+ "hostTargets": [
26
+ "darwin-arm64",
27
+ "darwin-x64"
28
+ ]
29
+ },
30
+ "files": [
31
+ "src/",
32
+ "skills/",
33
+ "openclaw.plugin.json"
34
+ ],
35
+ "keywords": [
36
+ "openclaw",
37
+ "openclaw-plugin",
38
+ "findmy",
39
+ "location",
40
+ "macos",
41
+ "ocr",
42
+ "vision",
43
+ "screen-scraping"
44
+ ],
45
+ "author": {
46
+ "name": "Omar Shahine",
47
+ "email": "omar@shahine.com"
48
+ },
49
+ "license": "MIT",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/omarshahine/findmy-cli.git",
53
+ "directory": "openclaw"
54
+ },
55
+ "homepage": "https://github.com/omarshahine/findmy-cli#readme",
56
+ "bugs": {
57
+ "url": "https://github.com/omarshahine/findmy-cli/issues"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ },
62
+ "os": [
63
+ "darwin"
64
+ ],
65
+ "engines": {
66
+ "node": ">=18"
67
+ },
68
+ "dependencies": {
69
+ "@sinclair/typebox": "^0.34.49",
70
+ "openclaw": "^2026.3.23"
71
+ },
72
+ "devDependencies": {
73
+ "@types/node": "^25.5.0",
74
+ "typescript": "^5.9.3"
75
+ },
76
+ "scripts": {
77
+ "plugin:check": "npx --yes @openclaw/plugin-inspector inspect --no-openclaw",
78
+ "plugin:ci": "npx --yes @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute"
79
+ }
80
+ }
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: findmy
3
+ description: |
4
+ Query Find My friend locations on macOS via the findmy-cli plugin tools.
5
+ Returns name, coarse location, staleness, and distance for everyone in
6
+ the FindMy.app People sidebar. Use when the user asks "where is X", "is
7
+ X home", "how far is X", or wants a location refresh.
8
+ ---
9
+
10
+ # Find My Location Query
11
+
12
+ Two tools available, both shell out to the `findmy` binary which drives
13
+ FindMy.app via screen capture and Vision OCR.
14
+
15
+ ## When to use
16
+
17
+ - "Where is Omar?" → call `findmy_person` with `name: "Omar"`
18
+ - "Is Sarah home yet?" → call `findmy_person` with `name: "Sarah"`
19
+ - "How far away is Mike?" → call `findmy_person` with `name: "Mike"`
20
+ - "Anyone near downtown?" → call `findmy_people`, then read the locations
21
+ - "Where is everyone?" → call `findmy_people`
22
+
23
+ ## Output shape
24
+
25
+ ```json
26
+ [
27
+ {
28
+ "name": "Omar Shahine",
29
+ "location": "Redmond, WA",
30
+ "staleness": "Paused",
31
+ "distance": "7 mi"
32
+ }
33
+ ]
34
+ ```
35
+
36
+ - `name` — display name as shown in the FindMy sidebar
37
+ - `location` — city, state (or device label when sharing from a device)
38
+ - `staleness` — `"Now"`, `"X min. ago"`, `"X hr. ago"`, `"Paused"`, `""` (live)
39
+ - `distance` — distance from this Mac if FindMy shows it (e.g. `"7 mi"`)
40
+
41
+ ## Caveats to surface to the user
42
+
43
+ - **`staleness: "Paused"`** means the friend paused location sharing. The
44
+ reported location is the last known position, possibly hours or days old.
45
+ Lead with this when reporting the result.
46
+ - **Stale staleness** (`"7 hr. ago"`, etc.) means the device hasn't checked
47
+ in recently — phone may be off, in low-power mode, or out of signal.
48
+ - **Focus steal**: each invocation briefly raises FindMy.app to the front.
49
+ - **Back-to-back races**: two findmy calls within ~5s can fail. Space them
50
+ out when iterating.
51
+
52
+ ## Install requirement
53
+
54
+ The plugin shells out to the `findmy` binary. If a tool returns
55
+ `"findmy not found on PATH"`, the binary isn't installed. Install with:
56
+
57
+ ```bash
58
+ brew install omarshahine/tap/findmy-cli
59
+ ```
60
+
61
+ After install, grant **Screen Recording** to the host process running this
62
+ plugin (System Settings → Privacy & Security → Screen Recording). Without
63
+ it, FindMy.app captures will return blank.
package/src/index.ts ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * OpenClaw plugin entry for findmy-cli.
3
+ *
4
+ * Registers two tools that shell out to the `findmy` binary to query Find My
5
+ * friend locations on macOS. The CLI drives FindMy.app via screen capture
6
+ * and Vision OCR — see the host repo for the underlying mechanism.
7
+ */
8
+
9
+ import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
10
+ import { Type } from '@sinclair/typebox';
11
+ import { execFileSync, execFile } from 'child_process';
12
+ import { promisify } from 'util';
13
+ import { existsSync } from 'fs';
14
+
15
+ const execFileAsync = promisify(execFile);
16
+
17
+ interface PluginConfig {
18
+ cliPath?: string;
19
+ }
20
+
21
+ interface ToolDef {
22
+ name: string;
23
+ description: string;
24
+ parameters: ReturnType<typeof Type.Object>;
25
+ buildArgs: (params: Record<string, unknown>) => string[];
26
+ }
27
+
28
+ const TOOLS: ToolDef[] = [
29
+ {
30
+ name: 'findmy_people',
31
+ description:
32
+ 'List every friend in the FindMy.app People sidebar. Returns name, coarse location (city, state), staleness, and distance for each person. Use for "who is where", "anyone near downtown", or any broad location query. Each entry includes a `staleness` field — if "Paused", the friend has paused location sharing and the location is the last known position.',
33
+ parameters: Type.Object({}),
34
+ buildArgs: () => ['people', '--json'],
35
+ },
36
+ {
37
+ name: 'findmy_person',
38
+ description:
39
+ 'Look up a single friend by name (case-insensitive substring match). Returns the same shape as findmy_people but filtered. Use for "where is X", "is X home", "how far is X". Match works on partial names — "sarah" matches "Sarah Shahine".',
40
+ parameters: Type.Object({
41
+ name: Type.String({
42
+ description: 'Friend name or substring (case-insensitive).',
43
+ }),
44
+ }),
45
+ buildArgs: (params) => ['person', String(params.name), '--json'],
46
+ },
47
+ ];
48
+
49
+ function toolResult(text: string) {
50
+ return {
51
+ content: [{ type: 'text' as const, text }],
52
+ details: undefined,
53
+ };
54
+ }
55
+
56
+ function whichBinary(name: string): string | null {
57
+ const cmd = process.platform === 'win32' ? 'where.exe' : 'which';
58
+ try {
59
+ const result = execFileSync(cmd, [name], { encoding: 'utf8' }).trim();
60
+ const first = result.split('\n')[0]?.trim();
61
+ return first || null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Resolve the findmy binary:
69
+ * 1. Plugin config cliPath
70
+ * 2. Env var FINDMY_CLI_PATH
71
+ * 3. PATH lookup
72
+ */
73
+ function resolveCliPath(config?: PluginConfig): string {
74
+ if (config?.cliPath && existsSync(config.cliPath)) {
75
+ return config.cliPath;
76
+ }
77
+
78
+ const envPath = process.env.FINDMY_CLI_PATH;
79
+ if (envPath && existsSync(envPath)) {
80
+ return envPath;
81
+ }
82
+
83
+ const found = whichBinary('findmy');
84
+ if (found) return found;
85
+
86
+ throw new Error(
87
+ 'findmy not found on PATH. Install with: brew install omarshahine/tap/findmy-cli\n' +
88
+ 'Or set FINDMY_CLI_PATH or configure cliPath in plugin settings.'
89
+ );
90
+ }
91
+
92
+ export default definePluginEntry({
93
+ id: 'findmy-cli',
94
+ name: 'Find My',
95
+ description: 'Query Find My friend locations on macOS via UI scraping',
96
+
97
+ register(api) {
98
+ const config = api.pluginConfig as PluginConfig | undefined;
99
+
100
+ let cliPath: string;
101
+ try {
102
+ cliPath = resolveCliPath(config);
103
+ } catch (error) {
104
+ const errorMessage = error instanceof Error ? error.message : String(error);
105
+ for (const tool of TOOLS) {
106
+ api.registerTool({
107
+ name: tool.name,
108
+ label: tool.name,
109
+ description: tool.description,
110
+ parameters: tool.parameters,
111
+ async execute() {
112
+ return toolResult(
113
+ JSON.stringify({ success: false, error: errorMessage }, null, 2)
114
+ );
115
+ },
116
+ });
117
+ }
118
+ return;
119
+ }
120
+
121
+ for (const tool of TOOLS) {
122
+ api.registerTool({
123
+ name: tool.name,
124
+ label: tool.name,
125
+ description: tool.description,
126
+ parameters: tool.parameters,
127
+
128
+ async execute(_id: string, params: Record<string, unknown>) {
129
+ try {
130
+ const args = tool.buildArgs(params);
131
+ const { stdout } = await execFileAsync(cliPath, args, {
132
+ encoding: 'utf8',
133
+ // FindMy.app capture is slow on cold boot — it has to launch,
134
+ // switch tabs, render the sidebar, and OCR a screenshot.
135
+ timeout: 60_000,
136
+ maxBuffer: 4 * 1024 * 1024,
137
+ });
138
+
139
+ if (stdout.trim().length === 0) {
140
+ return toolResult(JSON.stringify({ success: true }, null, 2));
141
+ }
142
+
143
+ let result: unknown;
144
+ try {
145
+ result = JSON.parse(stdout);
146
+ } catch {
147
+ result = { output: stdout.trim() };
148
+ }
149
+ return toolResult(JSON.stringify(result, null, 2));
150
+ } catch (error: unknown) {
151
+ const message = error instanceof Error ? error.message : String(error);
152
+ const stderr =
153
+ error && typeof error === 'object' && 'stderr' in error
154
+ ? String((error as { stderr: unknown }).stderr).trim()
155
+ : '';
156
+ const errorOutput = stderr ? `${message}\n\nstderr: ${stderr}` : message;
157
+ return toolResult(
158
+ JSON.stringify({ success: false, error: errorOutput }, null, 2)
159
+ );
160
+ }
161
+ },
162
+ });
163
+ }
164
+ },
165
+ });