palmier 0.8.11 → 0.9.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/README.md +3 -1
- package/dist/linked-device.d.ts +9 -0
- package/dist/linked-device.js +45 -0
- package/dist/mcp-tools.js +19 -19
- package/dist/pwa/assets/index-BLCVzS_l.js +120 -0
- package/dist/pwa/assets/{index-DhphickB.css → index-Cjjw24Ok.css} +1 -1
- package/dist/pwa/assets/{web-4WNPL7z3.js → web-C2AU9S9n.js} +1 -1
- package/dist/pwa/assets/{web-DjwsAB0V.js → web-CfD_ah7K.js} +1 -1
- package/dist/pwa/assets/{web-Bpd2nO1M.js → web-DugGj1t8.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +2 -2
- package/dist/rpc-handler.js +17 -23
- package/package.json +1 -1
- package/palmier-server/README.md +2 -1
- package/palmier-server/pwa/src/App.css +37 -0
- package/palmier-server/pwa/src/App.tsx +36 -15
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +65 -225
- package/palmier-server/pwa/src/components/HostMenu.tsx +110 -21
- package/palmier-server/pwa/src/components/RunDetailView.tsx +2 -2
- package/palmier-server/pwa/src/components/SessionComposer.tsx +9 -8
- package/palmier-server/pwa/src/components/SessionsView.tsx +5 -3
- package/palmier-server/pwa/src/components/TabBar.tsx +7 -5
- package/palmier-server/pwa/src/components/TaskForm.tsx +5 -3
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +41 -41
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +17 -60
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +6 -7
- package/palmier-server/pwa/src/native/Device.ts +23 -38
- package/palmier-server/pwa/src/pages/Dashboard.tsx +36 -41
- package/palmier-server/pwa/src/pages/PairHost.tsx +20 -1
- package/palmier-server/pwa/src/pages/PairSetup.tsx +98 -39
- package/palmier-server/pwa/src/service-worker.ts +9 -6
- package/palmier-server/pwa/src/types.ts +2 -0
- package/palmier-server/spec.md +37 -11
- package/src/linked-device.ts +52 -0
- package/src/mcp-tools.ts +19 -19
- package/src/rpc-handler.ts +14 -22
- package/dist/device-capabilities.d.ts +0 -9
- package/dist/device-capabilities.js +0 -36
- package/dist/pwa/assets/index-B7S0YoMo.js +0 -120
- package/src/device-capabilities.ts +0 -57
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ Palmier exposes an [MCP](https://modelcontextprotocol.io) server at `http://loca
|
|
|
78
78
|
|
|
79
79
|
Resources support MCP subscriptions — clients can subscribe via `resources/subscribe` and receive real-time `notifications/resources/updated` events via the streamable HTTP transport when the resource changes.
|
|
80
80
|
|
|
81
|
-
All device tools work while the Palmier Android app is in the background — they communicate via FCM data messages which wake the app's service even when it's not in the foreground. Permissions listed above must be granted via toggles in the
|
|
81
|
+
All device tools work while the Palmier Android app is in the background — they communicate via FCM data messages which wake the app's service even when it's not in the foreground. Each host has one **linked device**: the phone the host uses for SMS, contacts, location, and other device capabilities. Choose it at pair time (the "Link to this device" checkbox) or later from the drawer. Permissions listed above must be granted via toggles in the linked device's drawer.
|
|
82
82
|
|
|
83
83
|
### Architecture
|
|
84
84
|
|
|
@@ -151,6 +151,8 @@ palmier clients revoke <token>
|
|
|
151
151
|
palmier clients revoke-all
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
+
Revoking the linked device also clears the host's linked-device record; device capabilities stop working until another paired device is linked from its drawer.
|
|
155
|
+
|
|
154
156
|
### The `init` Command
|
|
155
157
|
|
|
156
158
|
The wizard:
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface LinkedDevice {
|
|
2
|
+
clientToken: string;
|
|
3
|
+
fcmToken: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function getLinkedDevice(): LinkedDevice | null;
|
|
6
|
+
export declare function setLinkedDevice(clientToken: string, fcmToken: string): void;
|
|
7
|
+
export declare function clearLinkedDevice(): void;
|
|
8
|
+
export declare function clearLinkedDeviceIfMatches(clientToken: string): boolean;
|
|
9
|
+
//# sourceMappingURL=linked-device.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { CONFIG_DIR } from "./config.js";
|
|
4
|
+
const LINKED_DEVICE_FILE = path.join(CONFIG_DIR, "linked-device.json");
|
|
5
|
+
function read() {
|
|
6
|
+
try {
|
|
7
|
+
if (!fs.existsSync(LINKED_DEVICE_FILE))
|
|
8
|
+
return null;
|
|
9
|
+
const raw = fs.readFileSync(LINKED_DEVICE_FILE, "utf-8");
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
if (!parsed?.clientToken || !parsed?.fcmToken)
|
|
12
|
+
return null;
|
|
13
|
+
return { clientToken: parsed.clientToken, fcmToken: parsed.fcmToken };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function write(device) {
|
|
20
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
if (!device) {
|
|
22
|
+
if (fs.existsSync(LINKED_DEVICE_FILE))
|
|
23
|
+
fs.unlinkSync(LINKED_DEVICE_FILE);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
fs.writeFileSync(LINKED_DEVICE_FILE, JSON.stringify(device, null, 2), "utf-8");
|
|
27
|
+
}
|
|
28
|
+
export function getLinkedDevice() {
|
|
29
|
+
return read();
|
|
30
|
+
}
|
|
31
|
+
export function setLinkedDevice(clientToken, fcmToken) {
|
|
32
|
+
write({ clientToken, fcmToken });
|
|
33
|
+
}
|
|
34
|
+
export function clearLinkedDevice() {
|
|
35
|
+
write(null);
|
|
36
|
+
}
|
|
37
|
+
export function clearLinkedDeviceIfMatches(clientToken) {
|
|
38
|
+
const current = read();
|
|
39
|
+
if (current?.clientToken === clientToken) {
|
|
40
|
+
write(null);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=linked-device.js.map
|
package/dist/mcp-tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StringCodec } from "nats";
|
|
2
2
|
import { registerPending } from "./pending-requests.js";
|
|
3
|
-
import {
|
|
3
|
+
import { getLinkedDevice } from "./linked-device.js";
|
|
4
4
|
import { getNotifications, onNotificationsChanged } from "./notification-store.js";
|
|
5
5
|
import { getSmsMessages, onSmsChanged } from "./sms-store.js";
|
|
6
6
|
export class ToolError extends Error {
|
|
@@ -154,9 +154,9 @@ const deviceGeolocationTool = {
|
|
|
154
154
|
async handler(_args, ctx) {
|
|
155
155
|
if (!ctx.nc)
|
|
156
156
|
throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
157
|
-
const device =
|
|
157
|
+
const device = getLinkedDevice();
|
|
158
158
|
if (!device)
|
|
159
|
-
throw new ToolError("No device
|
|
159
|
+
throw new ToolError("No linked device configured", 400);
|
|
160
160
|
const sc = StringCodec();
|
|
161
161
|
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.geolocation`, sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken })), { timeout: 5_000 });
|
|
162
162
|
const ack = JSON.parse(sc.decode(ackReply.data));
|
|
@@ -195,9 +195,9 @@ const readContactsTool = {
|
|
|
195
195
|
async handler(_args, ctx) {
|
|
196
196
|
if (!ctx.nc)
|
|
197
197
|
throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
198
|
-
const device =
|
|
198
|
+
const device = getLinkedDevice();
|
|
199
199
|
if (!device)
|
|
200
|
-
throw new ToolError("No device
|
|
200
|
+
throw new ToolError("No linked device configured", 400);
|
|
201
201
|
const sc = StringCodec();
|
|
202
202
|
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.contacts`, sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken, action: "read" })), { timeout: 5_000 });
|
|
203
203
|
const ack = JSON.parse(sc.decode(ackReply.data));
|
|
@@ -241,9 +241,9 @@ const createContactTool = {
|
|
|
241
241
|
async handler(args, ctx) {
|
|
242
242
|
if (!ctx.nc)
|
|
243
243
|
throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
244
|
-
const device =
|
|
244
|
+
const device = getLinkedDevice();
|
|
245
245
|
if (!device)
|
|
246
|
-
throw new ToolError("No device
|
|
246
|
+
throw new ToolError("No linked device configured", 400);
|
|
247
247
|
const { name, phone, email } = args;
|
|
248
248
|
if (!name)
|
|
249
249
|
throw new ToolError("name is required", 400);
|
|
@@ -292,9 +292,9 @@ const readCalendarTool = {
|
|
|
292
292
|
async handler(args, ctx) {
|
|
293
293
|
if (!ctx.nc)
|
|
294
294
|
throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
295
|
-
const device =
|
|
295
|
+
const device = getLinkedDevice();
|
|
296
296
|
if (!device)
|
|
297
|
-
throw new ToolError("No device
|
|
297
|
+
throw new ToolError("No linked device configured", 400);
|
|
298
298
|
const { startDate, endDate } = args;
|
|
299
299
|
const sc = StringCodec();
|
|
300
300
|
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.calendar`, sc.encode(JSON.stringify({
|
|
@@ -346,9 +346,9 @@ const createCalendarEventTool = {
|
|
|
346
346
|
async handler(args, ctx) {
|
|
347
347
|
if (!ctx.nc)
|
|
348
348
|
throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
349
|
-
const device =
|
|
349
|
+
const device = getLinkedDevice();
|
|
350
350
|
if (!device)
|
|
351
|
-
throw new ToolError("No device
|
|
351
|
+
throw new ToolError("No linked device configured", 400);
|
|
352
352
|
const { title, startTime, endTime, location, description } = args;
|
|
353
353
|
if (!title || !startTime || !endTime)
|
|
354
354
|
throw new ToolError("title, startTime, and endTime are required", 400);
|
|
@@ -400,7 +400,7 @@ const sendSmsTool = {
|
|
|
400
400
|
async handler(args, ctx) {
|
|
401
401
|
if (!ctx.nc)
|
|
402
402
|
throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
403
|
-
const device =
|
|
403
|
+
const device = getLinkedDevice();
|
|
404
404
|
if (!device)
|
|
405
405
|
throw new ToolError("No device has SMS Send enabled", 400);
|
|
406
406
|
const { to, body } = args;
|
|
@@ -452,9 +452,9 @@ const sendAlarmTool = {
|
|
|
452
452
|
async handler(args, ctx) {
|
|
453
453
|
if (!ctx.nc)
|
|
454
454
|
throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
455
|
-
const device =
|
|
455
|
+
const device = getLinkedDevice();
|
|
456
456
|
if (!device)
|
|
457
|
-
throw new ToolError("No device
|
|
457
|
+
throw new ToolError("No linked device configured", 400);
|
|
458
458
|
const { title, description } = args;
|
|
459
459
|
if (!title)
|
|
460
460
|
throw new ToolError("title is required", 400);
|
|
@@ -502,9 +502,9 @@ const readBatteryTool = {
|
|
|
502
502
|
async handler(_args, ctx) {
|
|
503
503
|
if (!ctx.nc)
|
|
504
504
|
throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
505
|
-
const device =
|
|
505
|
+
const device = getLinkedDevice();
|
|
506
506
|
if (!device)
|
|
507
|
-
throw new ToolError("No device
|
|
507
|
+
throw new ToolError("No linked device configured", 400);
|
|
508
508
|
const sc = StringCodec();
|
|
509
509
|
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.battery`, sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken })), { timeout: 5_000 });
|
|
510
510
|
const ack = JSON.parse(sc.decode(ackReply.data));
|
|
@@ -546,7 +546,7 @@ const setRingerModeTool = {
|
|
|
546
546
|
async handler(args, ctx) {
|
|
547
547
|
if (!ctx.nc)
|
|
548
548
|
throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
549
|
-
const device =
|
|
549
|
+
const device = getLinkedDevice();
|
|
550
550
|
if (!device)
|
|
551
551
|
throw new ToolError("No device has Do Not Disturb control enabled", 400);
|
|
552
552
|
const { mode } = args;
|
|
@@ -597,9 +597,9 @@ const sendEmailTool = {
|
|
|
597
597
|
async handler(args, ctx) {
|
|
598
598
|
if (!ctx.nc)
|
|
599
599
|
throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
600
|
-
const device =
|
|
600
|
+
const device = getLinkedDevice();
|
|
601
601
|
if (!device)
|
|
602
|
-
throw new ToolError("No device
|
|
602
|
+
throw new ToolError("No linked device configured", 400);
|
|
603
603
|
const { to, subject, body, cc, bcc } = args;
|
|
604
604
|
if (!to)
|
|
605
605
|
throw new ToolError("to is required", 400);
|