palmier 0.7.3 → 0.7.4

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/src/mcp-tools.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { StringCodec, type NatsConnection } from "nats";
2
2
  import { registerPending } from "./pending-requests.js";
3
- import { getLocationDevice } from "./location-device.js";
3
+ import { getCapabilityDevice } from "./device-capabilities.js";
4
4
  import { getNotifications } from "./notification-store.js";
5
5
  import { getSmsMessages } from "./sms-store.js";
6
6
  import type { HostConfig } from "./types.js";
@@ -170,14 +170,14 @@ const deviceGeolocationTool: ToolDefinition = {
170
170
  async handler(_args, ctx) {
171
171
  if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
172
172
 
173
- const locDevice = getLocationDevice();
174
- if (!locDevice) throw new ToolError("No device has location access enabled", 400);
173
+ const device = getCapabilityDevice("location");
174
+ if (!device) throw new ToolError("No device has location access enabled", 400);
175
175
 
176
176
  const sc = StringCodec();
177
177
 
178
178
  const ackReply = await ctx.nc.request(
179
179
  `host.${ctx.config.hostId}.fcm.geolocation`,
180
- sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: locDevice.fcmToken })),
180
+ sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken })),
181
181
  { timeout: 5_000 },
182
182
  );
183
183
  const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
@@ -218,11 +218,14 @@ const readContactsTool: ToolDefinition = {
218
218
  async handler(_args, ctx) {
219
219
  if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
220
220
 
221
+ const device = getCapabilityDevice("contacts");
222
+ if (!device) throw new ToolError("No device has contacts access enabled", 400);
223
+
221
224
  const sc = StringCodec();
222
225
 
223
226
  const ackReply = await ctx.nc.request(
224
227
  `host.${ctx.config.hostId}.fcm.contacts`,
225
- sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, action: "read" })),
228
+ sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken, action: "read" })),
226
229
  { timeout: 5_000 },
227
230
  );
228
231
  const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
@@ -268,6 +271,9 @@ const createContactTool: ToolDefinition = {
268
271
  async handler(args, ctx) {
269
272
  if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
270
273
 
274
+ const device = getCapabilityDevice("contacts");
275
+ if (!device) throw new ToolError("No device has contacts access enabled", 400);
276
+
271
277
  const { name, phone, email } = args as { name: string; phone?: string; email?: string };
272
278
  if (!name) throw new ToolError("name is required", 400);
273
279
 
@@ -276,7 +282,7 @@ const createContactTool: ToolDefinition = {
276
282
  const ackReply = await ctx.nc.request(
277
283
  `host.${ctx.config.hostId}.fcm.contacts`,
278
284
  sc.encode(JSON.stringify({
279
- hostId: ctx.config.hostId, requestId: ctx.sessionId,
285
+ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken,
280
286
  action: "create", name, phone, email,
281
287
  })),
282
288
  { timeout: 5_000 },
@@ -323,13 +329,16 @@ const readCalendarTool: ToolDefinition = {
323
329
  async handler(args, ctx) {
324
330
  if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
325
331
 
332
+ const device = getCapabilityDevice("calendar");
333
+ if (!device) throw new ToolError("No device has calendar access enabled", 400);
334
+
326
335
  const { startDate, endDate } = args as { startDate?: number; endDate?: number };
327
336
  const sc = StringCodec();
328
337
 
329
338
  const ackReply = await ctx.nc.request(
330
339
  `host.${ctx.config.hostId}.fcm.calendar`,
331
340
  sc.encode(JSON.stringify({
332
- hostId: ctx.config.hostId, requestId: ctx.sessionId,
341
+ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken,
333
342
  action: "read",
334
343
  ...(startDate ? { startDate: String(startDate) } : {}),
335
344
  ...(endDate ? { endDate: String(endDate) } : {}),
@@ -381,6 +390,9 @@ const createCalendarEventTool: ToolDefinition = {
381
390
  async handler(args, ctx) {
382
391
  if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
383
392
 
393
+ const device = getCapabilityDevice("calendar");
394
+ if (!device) throw new ToolError("No device has calendar access enabled", 400);
395
+
384
396
  const { title, startTime, endTime, location, description } = args as {
385
397
  title: string; startTime: number; endTime: number; location?: string; description?: string;
386
398
  };
@@ -391,7 +403,7 @@ const createCalendarEventTool: ToolDefinition = {
391
403
  const ackReply = await ctx.nc.request(
392
404
  `host.${ctx.config.hostId}.fcm.calendar`,
393
405
  sc.encode(JSON.stringify({
394
- hostId: ctx.config.hostId, requestId: ctx.sessionId,
406
+ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken,
395
407
  action: "create",
396
408
  title, startTime: String(startTime), endTime: String(endTime),
397
409
  ...(location ? { location } : {}),
@@ -424,7 +436,7 @@ const createCalendarEventTool: ToolDefinition = {
424
436
  };
425
437
 
426
438
  const sendSmsTool: ToolDefinition = {
427
- name: "send-sms",
439
+ name: "send-sms-message",
428
440
  description: [
429
441
  "Send an SMS message from the user's mobile device.",
430
442
  "Blocks until the device responds (up to 30 seconds).",
@@ -441,6 +453,9 @@ const sendSmsTool: ToolDefinition = {
441
453
  async handler(args, ctx) {
442
454
  if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
443
455
 
456
+ const device = getCapabilityDevice("sms");
457
+ if (!device) throw new ToolError("No device has SMS access enabled", 400);
458
+
444
459
  const { to, body } = args as { to: string; body: string };
445
460
  if (!to || !body) throw new ToolError("to and body are required", 400);
446
461
 
@@ -449,7 +464,7 @@ const sendSmsTool: ToolDefinition = {
449
464
  const ackReply = await ctx.nc.request(
450
465
  `host.${ctx.config.hostId}.fcm.sms`,
451
466
  sc.encode(JSON.stringify({
452
- hostId: ctx.config.hostId, requestId: ctx.sessionId,
467
+ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken,
453
468
  action: "send", to, body,
454
469
  })),
455
470
  { timeout: 5_000 },
@@ -478,44 +493,41 @@ const sendSmsTool: ToolDefinition = {
478
493
  },
479
494
  };
480
495
 
481
- const setAlarmTool: ToolDefinition = {
482
- name: "set-alarm",
496
+ const sendAlertTool: ToolDefinition = {
497
+ name: "send-alert",
483
498
  description: [
484
- "Set an alarm on the user's mobile device.",
499
+ "Send an alert to the user's mobile device with an alarm sound and full-screen popup.",
500
+ "Use this to urgently get the user's attention. The device will play an alarm sound and show a full-screen dialog even on the lock screen.",
485
501
  "Blocks until the device responds (up to 30 seconds).",
486
502
  'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
487
503
  ],
488
504
  inputSchema: {
489
505
  type: "object",
490
506
  properties: {
491
- hour: { type: "number", description: "Hour (0-23)" },
492
- minutes: { type: "number", description: "Minutes (0-59)" },
493
- label: { type: "string", description: "Alarm label" },
494
- days: {
495
- type: "array",
496
- items: { type: "number" },
497
- description: "Recurring days (1=Sun, 2=Mon, ..., 7=Sat). Omit for one-time.",
498
- },
507
+ title: { type: "string", description: "Alert title" },
508
+ description: { type: "string", description: "Alert description/details" },
499
509
  },
500
- required: ["hour", "minutes"],
510
+ required: ["title"],
501
511
  },
502
512
  async handler(args, ctx) {
503
513
  if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
504
514
 
505
- const { hour, minutes, label, days } = args as { hour: number; minutes: number; label?: string; days?: number[] };
506
- if (hour == null || minutes == null) throw new ToolError("hour and minutes are required", 400);
515
+ const device = getCapabilityDevice("alert");
516
+ if (!device) throw new ToolError("No device has alert access enabled", 400);
517
+
518
+ const { title, description } = args as { title: string; description?: string };
519
+ if (!title) throw new ToolError("title is required", 400);
507
520
 
508
521
  const sc = StringCodec();
509
522
 
510
- const payload: Record<string, unknown> = {
511
- hostId: ctx.config.hostId, requestId: ctx.sessionId,
512
- action: "set", hour: String(hour), minutes: String(minutes),
523
+ const payload: Record<string, string> = {
524
+ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken,
525
+ title,
513
526
  };
514
- if (label) payload.label = label;
515
- if (days?.length) payload.days = days.join(",");
527
+ if (description) payload.description = description;
516
528
 
517
529
  const ackReply = await ctx.nc.request(
518
- `host.${ctx.config.hostId}.fcm.alarm`,
530
+ `host.${ctx.config.hostId}.fcm.alert`,
519
531
  sc.encode(JSON.stringify(payload)),
520
532
  { timeout: 5_000 },
521
533
  );
@@ -523,7 +535,7 @@ const setAlarmTool: ToolDefinition = {
523
535
  if (ack.error) throw new ToolError(ack.error, 502);
524
536
 
525
537
  const responsePromise = new Promise<string>((resolve, reject) => {
526
- const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.alarm.${ctx.sessionId}`, { max: 1 });
538
+ const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.alert.${ctx.sessionId}`, { max: 1 });
527
539
  const timer = setTimeout(() => {
528
540
  sub.unsubscribe();
529
541
  reject(new ToolError("Device did not respond within 30 seconds", 504));
@@ -557,11 +569,14 @@ const readBatteryTool: ToolDefinition = {
557
569
  async handler(_args, ctx) {
558
570
  if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
559
571
 
572
+ const device = getCapabilityDevice("battery");
573
+ if (!device) throw new ToolError("No device has battery access enabled", 400);
574
+
560
575
  const sc = StringCodec();
561
576
 
562
577
  const ackReply = await ctx.nc.request(
563
578
  `host.${ctx.config.hostId}.fcm.battery`,
564
- sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId })),
579
+ sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken })),
565
580
  { timeout: 5_000 },
566
581
  );
567
582
  const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
@@ -605,6 +620,9 @@ const setRingerModeTool: ToolDefinition = {
605
620
  async handler(args, ctx) {
606
621
  if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
607
622
 
623
+ const device = getCapabilityDevice("dnd");
624
+ if (!device) throw new ToolError("No device has Do Not Disturb control enabled", 400);
625
+
608
626
  const { mode } = args as { mode: string };
609
627
  if (!["normal", "vibrate", "silent"].includes(mode)) throw new ToolError("mode must be 'normal', 'vibrate', or 'silent'", 400);
610
628
 
@@ -612,7 +630,7 @@ const setRingerModeTool: ToolDefinition = {
612
630
 
613
631
  const ackReply = await ctx.nc.request(
614
632
  `host.${ctx.config.hostId}.fcm.ringer`,
615
- sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, mode })),
633
+ sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken, mode })),
616
634
  { timeout: 5_000 },
617
635
  );
618
636
  const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
@@ -639,7 +657,7 @@ const setRingerModeTool: ToolDefinition = {
639
657
  },
640
658
  };
641
659
 
642
- export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, setAlarmTool, readBatteryTool, setRingerModeTool];
660
+ export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendAlertTool, readBatteryTool, setRingerModeTool];
643
661
  export const agentToolMap = new Map<string, ToolDefinition>(agentTools.map((t) => [t.name, t]));
644
662
 
645
663
  // ── MCP Resources ─────────────────────────────────────────────────────
@@ -671,14 +689,14 @@ const deviceNotificationsResource: ResourceDefinition = {
671
689
  };
672
690
 
673
691
  const deviceSmsResource: ResourceDefinition = {
674
- uri: "sms://device",
692
+ uri: "sms-messages://device",
675
693
  name: "Device SMS",
676
694
  description: [
677
695
  "Get recent SMS messages from the user's Android device.",
678
696
  "Response: JSON array of message objects with `id`, `sender`, `body`, `timestamp`.",
679
697
  ],
680
698
  mimeType: "application/json",
681
- restPath: "/sms",
699
+ restPath: "/sms-messages",
682
700
  read: getSmsMessages,
683
701
  };
684
702
 
@@ -11,7 +11,7 @@ import crossSpawn from "cross-spawn";
11
11
  import { getAgent } from "./agents/agent.js";
12
12
  import { validateClient } from "./client-store.js";
13
13
  import { publishHostEvent } from "./events.js";
14
- import { getLocationDevice, setLocationDevice, clearLocationDevice } from "./location-device.js";
14
+ import { getCapabilityDevice, setCapabilityDevice, clearCapabilityDevice, type DeviceCapability } from "./device-capabilities.js";
15
15
  import { currentVersion, performUpdate } from "./update-checker.js";
16
16
  import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
17
17
  import type { HostConfig, ParsedTask, RpcMessage, ConversationMessage } from "./types.js";
@@ -163,7 +163,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
163
163
  switch (request.method) {
164
164
  case "task.list": {
165
165
  const tasks = listTasks(config.projectRoot);
166
- const locDevice = getLocationDevice();
166
+ const locDevice = getCapabilityDevice("location");
167
167
  return {
168
168
  tasks: tasks.map((task) => flattenTask(task)),
169
169
  agents: config.agents ?? [],
@@ -652,12 +652,27 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
652
652
  const params = request.params as { fcmToken: string };
653
653
  if (!params.fcmToken) return { error: "fcmToken is required" };
654
654
  const clientToken = request.clientToken ?? "";
655
- setLocationDevice(clientToken, params.fcmToken);
655
+ setCapabilityDevice("location", clientToken, params.fcmToken);
656
656
  return { ok: true };
657
657
  }
658
658
 
659
659
  case "device.location.disable": {
660
- clearLocationDevice();
660
+ clearCapabilityDevice("location");
661
+ return { ok: true };
662
+ }
663
+
664
+ case "device.capability.enable": {
665
+ const params = request.params as { capability: DeviceCapability; fcmToken: string };
666
+ if (!params.capability || !params.fcmToken) return { error: "capability and fcmToken are required" };
667
+ const clientToken = request.clientToken ?? "";
668
+ setCapabilityDevice(params.capability, clientToken, params.fcmToken);
669
+ return { ok: true };
670
+ }
671
+
672
+ case "device.capability.disable": {
673
+ const params = request.params as { capability: DeviceCapability };
674
+ if (!params.capability) return { error: "capability is required" };
675
+ clearCapabilityDevice(params.capability);
661
676
  return { ok: true };
662
677
  }
663
678
 
@@ -125,7 +125,7 @@ export async function startHttpTransport(
125
125
 
126
126
  // Wire up resource change listeners
127
127
  onNotificationsChanged(() => broadcastResourceUpdated("notifications://device"));
128
- onSmsChanged(() => broadcastResourceUpdated("sms://device"));
128
+ onSmsChanged(() => broadcastResourceUpdated("sms-messages://device"));
129
129
 
130
130
  // If a pairing code is provided, pre-register it
131
131
  if (pairingCode) {
@@ -1,8 +0,0 @@
1
- export interface LocationDevice {
2
- clientToken: string;
3
- fcmToken: string;
4
- }
5
- export declare function getLocationDevice(): LocationDevice | null;
6
- export declare function setLocationDevice(clientToken: string, fcmToken: string): void;
7
- export declare function clearLocationDevice(): void;
8
- //# sourceMappingURL=location-device.d.ts.map
@@ -1,32 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { CONFIG_DIR } from "./config.js";
4
- const LOCATION_FILE = path.join(CONFIG_DIR, "location-device.json");
5
- export function getLocationDevice() {
6
- try {
7
- if (!fs.existsSync(LOCATION_FILE))
8
- return null;
9
- const raw = fs.readFileSync(LOCATION_FILE, "utf-8");
10
- const data = JSON.parse(raw);
11
- if (!data.clientToken || !data.fcmToken)
12
- return null;
13
- return data;
14
- }
15
- catch {
16
- return null;
17
- }
18
- }
19
- export function setLocationDevice(clientToken, fcmToken) {
20
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
21
- fs.writeFileSync(LOCATION_FILE, JSON.stringify({ clientToken, fcmToken }, null, 2), "utf-8");
22
- }
23
- export function clearLocationDevice() {
24
- try {
25
- if (fs.existsSync(LOCATION_FILE))
26
- fs.unlinkSync(LOCATION_FILE);
27
- }
28
- catch {
29
- // ignore
30
- }
31
- }
32
- //# sourceMappingURL=location-device.js.map