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.
@@ -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
+ };
@@ -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.0",
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.0",
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
  }
@@ -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 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.
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 Location Query
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', String(params.name), '--json'],
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,