findmy-cli 0.1.0 → 0.1.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/dist/index.d.ts +27 -0
- package/dist/index.js +150 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -1
- package/skills/findmy/SKILL.md +33 -5
- package/src/index.ts +40 -1
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as openclaw_plugin_sdk_plugin_entry from 'openclaw/plugin-sdk/plugin-entry';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenClaw plugin entry for findmy-cli.
|
|
5
|
+
*
|
|
6
|
+
* Registers two tools that shell out to the `findmy` binary to query Find My
|
|
7
|
+
* friend locations on macOS. The CLI drives FindMy.app via screen capture
|
|
8
|
+
* and Vision OCR — see the host repo for the underlying mechanism.
|
|
9
|
+
*
|
|
10
|
+
* Security posture:
|
|
11
|
+
* - Spawns via execFile (NOT exec / shell): argv is passed as a token array,
|
|
12
|
+
* so user-controlled strings cannot inject shell metacharacters.
|
|
13
|
+
* - Read-only: never writes, deletes, or mutates anything. No network I/O
|
|
14
|
+
* from this process (the underlying findmy binary stays on-device too).
|
|
15
|
+
* - No eval, Function(), dynamic import, or curl|sh install steps.
|
|
16
|
+
* - User input (`name` for findmy_person) is length-bounded and ASCII-class
|
|
17
|
+
* validated below before passing to execFile.
|
|
18
|
+
*/
|
|
19
|
+
declare const _default: {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
configSchema: openclaw_plugin_sdk_plugin_entry.OpenClawPluginConfigSchema;
|
|
24
|
+
register: NonNullable<openclaw_plugin_sdk_plugin_entry.OpenClawPluginDefinition["register"]>;
|
|
25
|
+
} & Pick<openclaw_plugin_sdk_plugin_entry.OpenClawPluginDefinition, "kind" | "reload" | "nodeHostCommands" | "securityAuditCollectors">;
|
|
26
|
+
|
|
27
|
+
export { _default as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import { execFileSync, execFile } from "child_process";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
var execFileAsync = promisify(execFile);
|
|
8
|
+
var MAX_NAME_LENGTH = 100;
|
|
9
|
+
var NAME_ALLOWLIST = /^[\p{L}\p{M}\p{N} .'\-]+$/u;
|
|
10
|
+
function validateName(raw) {
|
|
11
|
+
if (typeof raw !== "string") {
|
|
12
|
+
throw new Error("name must be a string");
|
|
13
|
+
}
|
|
14
|
+
const trimmed = raw.trim();
|
|
15
|
+
if (trimmed.length === 0) {
|
|
16
|
+
throw new Error("name must not be empty");
|
|
17
|
+
}
|
|
18
|
+
if (trimmed.length > MAX_NAME_LENGTH) {
|
|
19
|
+
throw new Error(`name must be ${MAX_NAME_LENGTH} characters or fewer`);
|
|
20
|
+
}
|
|
21
|
+
if (!NAME_ALLOWLIST.test(trimmed)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"name contains unsupported characters (letters, spaces, hyphens, apostrophes, periods only)"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return trimmed;
|
|
27
|
+
}
|
|
28
|
+
var TOOLS = [
|
|
29
|
+
{
|
|
30
|
+
name: "findmy_people",
|
|
31
|
+
description: '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 \u2014 if "Paused", the friend has paused location sharing and the location is the last known position.',
|
|
32
|
+
parameters: Type.Object({}),
|
|
33
|
+
buildArgs: () => ["people", "--json"]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "findmy_person",
|
|
37
|
+
description: '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 \u2014 "sarah" matches "Sarah Shahine".',
|
|
38
|
+
parameters: Type.Object({
|
|
39
|
+
name: Type.String({
|
|
40
|
+
description: "Friend name or substring (case-insensitive).",
|
|
41
|
+
maxLength: MAX_NAME_LENGTH
|
|
42
|
+
})
|
|
43
|
+
}),
|
|
44
|
+
buildArgs: (params) => ["person", validateName(params.name), "--json"]
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
function toolResult(text) {
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text }],
|
|
50
|
+
details: void 0
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function whichBinary(name) {
|
|
54
|
+
const cmd = process.platform === "win32" ? "where.exe" : "which";
|
|
55
|
+
try {
|
|
56
|
+
const result = execFileSync(cmd, [name], { encoding: "utf8" }).trim();
|
|
57
|
+
const first = result.split("\n")[0]?.trim();
|
|
58
|
+
return first || null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function resolveCliPath(config) {
|
|
64
|
+
if (config?.cliPath && existsSync(config.cliPath)) {
|
|
65
|
+
return config.cliPath;
|
|
66
|
+
}
|
|
67
|
+
const envPath = process.env.FINDMY_CLI_PATH;
|
|
68
|
+
if (envPath && existsSync(envPath)) {
|
|
69
|
+
return envPath;
|
|
70
|
+
}
|
|
71
|
+
const found = whichBinary("findmy");
|
|
72
|
+
if (found) return found;
|
|
73
|
+
throw new Error(
|
|
74
|
+
"findmy not found on PATH. Install with: brew install omarshahine/tap/findmy-cli\nOr set FINDMY_CLI_PATH or configure cliPath in plugin settings."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
var index_default = definePluginEntry({
|
|
78
|
+
id: "findmy-cli",
|
|
79
|
+
name: "Find My",
|
|
80
|
+
description: "Query Find My friend locations on macOS via UI scraping",
|
|
81
|
+
register(api) {
|
|
82
|
+
const config = api.pluginConfig;
|
|
83
|
+
let cliPath;
|
|
84
|
+
try {
|
|
85
|
+
cliPath = resolveCliPath(config);
|
|
86
|
+
console.error(`[findmy-cli] registered (binary at ${cliPath})`);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
89
|
+
console.error(
|
|
90
|
+
`[findmy-cli] registered without a working binary: ${errorMessage}`
|
|
91
|
+
);
|
|
92
|
+
for (const tool of TOOLS) {
|
|
93
|
+
api.registerTool({
|
|
94
|
+
name: tool.name,
|
|
95
|
+
label: tool.name,
|
|
96
|
+
description: tool.description,
|
|
97
|
+
parameters: tool.parameters,
|
|
98
|
+
async execute() {
|
|
99
|
+
return toolResult(
|
|
100
|
+
JSON.stringify({ success: false, error: errorMessage }, null, 2)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (const tool of TOOLS) {
|
|
108
|
+
api.registerTool({
|
|
109
|
+
name: tool.name,
|
|
110
|
+
label: tool.name,
|
|
111
|
+
description: tool.description,
|
|
112
|
+
parameters: tool.parameters,
|
|
113
|
+
async execute(_id, params) {
|
|
114
|
+
try {
|
|
115
|
+
const args = tool.buildArgs(params);
|
|
116
|
+
const { stdout } = await execFileAsync(cliPath, args, {
|
|
117
|
+
encoding: "utf8",
|
|
118
|
+
// FindMy.app capture is slow on cold boot — it has to launch,
|
|
119
|
+
// switch tabs, render the sidebar, and OCR a screenshot.
|
|
120
|
+
timeout: 6e4,
|
|
121
|
+
maxBuffer: 4 * 1024 * 1024
|
|
122
|
+
});
|
|
123
|
+
if (stdout.trim().length === 0) {
|
|
124
|
+
return toolResult(JSON.stringify({ success: true }, null, 2));
|
|
125
|
+
}
|
|
126
|
+
let result;
|
|
127
|
+
try {
|
|
128
|
+
result = JSON.parse(stdout);
|
|
129
|
+
} catch {
|
|
130
|
+
result = { output: stdout.trim() };
|
|
131
|
+
}
|
|
132
|
+
return toolResult(JSON.stringify(result, null, 2));
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
135
|
+
const stderr = error && typeof error === "object" && "stderr" in error ? String(error.stderr).trim() : "";
|
|
136
|
+
const errorOutput = stderr ? `${message}
|
|
137
|
+
|
|
138
|
+
stderr: ${stderr}` : message;
|
|
139
|
+
return toolResult(
|
|
140
|
+
JSON.stringify({ success: false, error: errorOutput }, null, 2)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
export {
|
|
149
|
+
index_default as default
|
|
150
|
+
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "findmy-cli",
|
|
3
3
|
"name": "Find My",
|
|
4
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.
|
|
5
|
+
"version": "0.1.2",
|
|
6
6
|
"platforms": [
|
|
7
7
|
"darwin"
|
|
8
8
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "findmy-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"openclaw": {
|
|
@@ -28,10 +28,12 @@
|
|
|
28
28
|
]
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
|
+
"dist/",
|
|
31
32
|
"src/",
|
|
32
33
|
"skills/",
|
|
33
34
|
"openclaw.plugin.json"
|
|
34
35
|
],
|
|
36
|
+
"main": "./dist/index.js",
|
|
35
37
|
"keywords": [
|
|
36
38
|
"openclaw",
|
|
37
39
|
"openclaw-plugin",
|
|
@@ -71,9 +73,11 @@
|
|
|
71
73
|
},
|
|
72
74
|
"devDependencies": {
|
|
73
75
|
"@types/node": "^25.5.0",
|
|
76
|
+
"tsup": "^8.5.0",
|
|
74
77
|
"typescript": "^5.9.3"
|
|
75
78
|
},
|
|
76
79
|
"scripts": {
|
|
80
|
+
"build": "tsup src/index.ts --format esm --out-dir dist --dts --clean --target node20",
|
|
77
81
|
"plugin:check": "npx --yes @openclaw/plugin-inspector inspect --no-openclaw",
|
|
78
82
|
"plugin:ci": "npx --yes @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute"
|
|
79
83
|
}
|
package/skills/findmy/SKILL.md
CHANGED
|
@@ -2,15 +2,32 @@
|
|
|
2
2
|
name: findmy
|
|
3
3
|
description: |
|
|
4
4
|
Query Find My friend locations on macOS via the findmy-cli plugin tools.
|
|
5
|
-
Returns name, coarse location, staleness, and distance for
|
|
6
|
-
the FindMy.app People sidebar. Use when the user asks "where
|
|
7
|
-
X home", "how far is X", or wants a location refresh.
|
|
5
|
+
Returns name, coarse location (city, state), staleness, and distance for
|
|
6
|
+
everyone in the FindMy.app People sidebar. Use when the user asks "where
|
|
7
|
+
is X", "is X home", "how far is X", or wants a location refresh.
|
|
8
|
+
license: MIT
|
|
9
|
+
metadata:
|
|
10
|
+
author: Omar Shahine
|
|
11
|
+
version: 0.1.2
|
|
12
|
+
openclaw:
|
|
13
|
+
emoji: pushpin
|
|
14
|
+
os: [darwin]
|
|
15
|
+
homepage: https://github.com/omarshahine/findmy-cli
|
|
16
|
+
requires:
|
|
17
|
+
bins: [findmy, findmy-helper]
|
|
18
|
+
install:
|
|
19
|
+
- kind: brew
|
|
20
|
+
id: findmy-cli
|
|
21
|
+
label: "Install findmy and findmy-helper via Homebrew"
|
|
22
|
+
formula: omarshahine/tap/findmy-cli
|
|
23
|
+
bins: [findmy, findmy-helper]
|
|
8
24
|
---
|
|
9
25
|
|
|
10
|
-
# Find My
|
|
26
|
+
# Find My Skill
|
|
11
27
|
|
|
12
28
|
Two tools available, both shell out to the `findmy` binary which drives
|
|
13
|
-
FindMy.app via screen capture and Vision OCR.
|
|
29
|
+
FindMy.app via screen capture and Vision OCR. Read-only — never mutates
|
|
30
|
+
FindMy.app state.
|
|
14
31
|
|
|
15
32
|
## When to use
|
|
16
33
|
|
|
@@ -61,3 +78,14 @@ brew install omarshahine/tap/findmy-cli
|
|
|
61
78
|
After install, grant **Screen Recording** to the host process running this
|
|
62
79
|
plugin (System Settings → Privacy & Security → Screen Recording). Without
|
|
63
80
|
it, FindMy.app captures will return blank.
|
|
81
|
+
|
|
82
|
+
## ClawScan note
|
|
83
|
+
|
|
84
|
+
This skill drives FindMy.app by raising it to the foreground, capturing a
|
|
85
|
+
screenshot of its window, running Apple's Vision OCR on the image, and
|
|
86
|
+
parsing the resulting text. The behavior may look unusual to a static
|
|
87
|
+
scanner — screen capture, OCR, and UI scraping — but it is the only path
|
|
88
|
+
to friend location data, since Apple does not expose this through any
|
|
89
|
+
public API. The plugin does not click, type into, or otherwise mutate
|
|
90
|
+
FindMy.app; it is read-only. No network traffic is initiated by this
|
|
91
|
+
plugin. All data stays on-device.
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
* Registers two tools that shell out to the `findmy` binary to query Find My
|
|
5
5
|
* friend locations on macOS. The CLI drives FindMy.app via screen capture
|
|
6
6
|
* and Vision OCR — see the host repo for the underlying mechanism.
|
|
7
|
+
*
|
|
8
|
+
* Security posture:
|
|
9
|
+
* - Spawns via execFile (NOT exec / shell): argv is passed as a token array,
|
|
10
|
+
* so user-controlled strings cannot inject shell metacharacters.
|
|
11
|
+
* - Read-only: never writes, deletes, or mutates anything. No network I/O
|
|
12
|
+
* from this process (the underlying findmy binary stays on-device too).
|
|
13
|
+
* - No eval, Function(), dynamic import, or curl|sh install steps.
|
|
14
|
+
* - User input (`name` for findmy_person) is length-bounded and ASCII-class
|
|
15
|
+
* validated below before passing to execFile.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
|
|
@@ -14,6 +23,31 @@ import { existsSync } from 'fs';
|
|
|
14
23
|
|
|
15
24
|
const execFileAsync = promisify(execFile);
|
|
16
25
|
|
|
26
|
+
const MAX_NAME_LENGTH = 100;
|
|
27
|
+
// Friend names from FindMy are real human names — letters, spaces, hyphens,
|
|
28
|
+
// apostrophes, periods. Reject anything outside that to keep argv clean and
|
|
29
|
+
// give the scanner a clear sanitization signal.
|
|
30
|
+
const NAME_ALLOWLIST = /^[\p{L}\p{M}\p{N} .'\-]+$/u;
|
|
31
|
+
|
|
32
|
+
function validateName(raw: unknown): string {
|
|
33
|
+
if (typeof raw !== 'string') {
|
|
34
|
+
throw new Error('name must be a string');
|
|
35
|
+
}
|
|
36
|
+
const trimmed = raw.trim();
|
|
37
|
+
if (trimmed.length === 0) {
|
|
38
|
+
throw new Error('name must not be empty');
|
|
39
|
+
}
|
|
40
|
+
if (trimmed.length > MAX_NAME_LENGTH) {
|
|
41
|
+
throw new Error(`name must be ${MAX_NAME_LENGTH} characters or fewer`);
|
|
42
|
+
}
|
|
43
|
+
if (!NAME_ALLOWLIST.test(trimmed)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'name contains unsupported characters (letters, spaces, hyphens, apostrophes, periods only)'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return trimmed;
|
|
49
|
+
}
|
|
50
|
+
|
|
17
51
|
interface PluginConfig {
|
|
18
52
|
cliPath?: string;
|
|
19
53
|
}
|
|
@@ -40,9 +74,10 @@ const TOOLS: ToolDef[] = [
|
|
|
40
74
|
parameters: Type.Object({
|
|
41
75
|
name: Type.String({
|
|
42
76
|
description: 'Friend name or substring (case-insensitive).',
|
|
77
|
+
maxLength: MAX_NAME_LENGTH,
|
|
43
78
|
}),
|
|
44
79
|
}),
|
|
45
|
-
buildArgs: (params) => ['person',
|
|
80
|
+
buildArgs: (params) => ['person', validateName(params.name), '--json'],
|
|
46
81
|
},
|
|
47
82
|
];
|
|
48
83
|
|
|
@@ -100,8 +135,12 @@ export default definePluginEntry({
|
|
|
100
135
|
let cliPath: string;
|
|
101
136
|
try {
|
|
102
137
|
cliPath = resolveCliPath(config);
|
|
138
|
+
console.error(`[findmy-cli] registered (binary at ${cliPath})`);
|
|
103
139
|
} catch (error) {
|
|
104
140
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
141
|
+
console.error(
|
|
142
|
+
`[findmy-cli] registered without a working binary: ${errorMessage}`
|
|
143
|
+
);
|
|
105
144
|
for (const tool of TOOLS) {
|
|
106
145
|
api.registerTool({
|
|
107
146
|
name: tool.name,
|