palmier 0.7.0 → 0.7.3

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 CHANGED
@@ -34,7 +34,7 @@ It runs on your machine as a background daemon and connects to a mobile-friendly
34
34
 
35
35
  ## How It Works
36
36
 
37
- Palmier runs as a background daemon (systemd on Linux, Task Scheduler on Windows). It invokes your agent CLIs directly, schedules tasks via native OS timers, and exposes an API that the PWA connects to — either directly over HTTP or remotely through a relay server. Agents can interact with the user's mobile device during execution — requesting input, sending push notifications, fetching GPS location, and reading device notifications.
37
+ Palmier runs as a background daemon (systemd on Linux, Task Scheduler on Windows). It invokes your agent CLIs directly, schedules tasks via native OS timers, and exposes an API that the PWA connects to — either directly over HTTP or remotely through a relay server. Agents can interact with the user's mobile device during execution — requesting input, sending push notifications, reading SMS/notifications, managing contacts and calendar, setting alarms, and more.
38
38
 
39
39
  ### MCP Server
40
40
 
@@ -43,19 +43,30 @@ Palmier exposes an [MCP](https://modelcontextprotocol.io) server at `http://loca
43
43
  **MCP server URL:** `http://localhost:<port>/mcp`
44
44
 
45
45
  **Available tools:**
46
- | Tool | Description |
47
- |------|-------------|
48
- | `notify` | Send a push notification to the user's device |
49
- | `request-input` | Request input from the user (blocks until response) |
50
- | `request-confirmation` | Request confirmation from the user (blocks until response) |
51
- | `device-geolocation` | Get GPS location of the user's mobile device |
46
+ | Tool | Description | Permission |
47
+ |------|-------------|------------|
48
+ | `notify` | Send a push notification to the user's device | None |
49
+ | `request-input` | Request input from the user (blocks until response) | None |
50
+ | `request-confirmation` | Request confirmation from the user (blocks until response) | None |
51
+ | `device-geolocation` | Get GPS location of the user's mobile device | Location Access |
52
+ | `read-contacts` | Read the contact list from the user's device | Contacts Access |
53
+ | `create-contact` | Create a new contact on the user's device | Contacts Access |
54
+ | `read-calendar` | Read calendar events (with time range filter) | Calendar Access |
55
+ | `create-calendar-event` | Create a calendar event on the user's device | Calendar Access |
56
+ | `send-sms` | Send an SMS message from the user's device | SMS Access |
57
+ | `set-alarm` | Set an alarm on the user's device | None |
58
+ | `read-battery` | Get battery level and charging status | None |
59
+ | `set-ringer-mode` | Set ringer mode (normal/vibrate/silent) | Do Not Disturb Control |
52
60
 
53
61
  **Available resources:**
54
62
  | Resource | URI | REST | Description |
55
63
  |----------|-----|------|-------------|
56
64
  | Device Notifications | `notifications://device` | `GET /notifications` | Recent notifications from the user's Android device |
65
+ | Device SMS | `sms://device` | `GET /sms` | Recent SMS messages from the user's Android device |
57
66
 
58
- 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. The Android app requires notification listener access to be enabled in system settings.
67
+ 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.
68
+
69
+ 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 Android app's settings menu.
59
70
 
60
71
  ```
61
72
  ┌──────────────┐ HTTP ┌──────────────────┐
@@ -13,6 +13,7 @@ import { saveConfig } from "../config.js";
13
13
  import { CONFIG_DIR } from "../config.js";
14
14
  import { StringCodec } from "nats";
15
15
  import { addNotification } from "../notification-store.js";
16
+ import { addSmsMessage } from "../sms-store.js";
16
17
  const POLL_INTERVAL_MS = 30_000;
17
18
  const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
18
19
  /**
@@ -116,7 +117,7 @@ export async function serveCommand() {
116
117
  // Start NATS transport (loops forever, fire-and-forget)
117
118
  if (nc) {
118
119
  startNatsTransport(config, handleRpc, nc);
119
- // Subscribe to device notifications from Android
120
+ // Subscribe to device notifications and SMS from Android
120
121
  const sc = StringCodec();
121
122
  const notifSub = nc.subscribe(`host.${config.hostId}.device.notifications`);
122
123
  (async () => {
@@ -130,6 +131,18 @@ export async function serveCommand() {
130
131
  }
131
132
  }
132
133
  })();
134
+ const smsSub = nc.subscribe(`host.${config.hostId}.device.sms`);
135
+ (async () => {
136
+ for await (const msg of smsSub) {
137
+ try {
138
+ const data = JSON.parse(sc.decode(msg.data));
139
+ addSmsMessage({ ...data, receivedAt: Date.now() });
140
+ }
141
+ catch (err) {
142
+ console.error("[nats] Failed to parse device SMS:", err);
143
+ }
144
+ }
145
+ })();
133
146
  }
134
147
  // Start HTTP transport (loops forever)
135
148
  await startHttpTransport(config, handleRpc, httpPort, nc);
@@ -127,12 +127,15 @@ export async function handleMcpRequest(body, sessionId, ctx) {
127
127
  if (!resource) {
128
128
  return { body: rpcError(id, -32602, `Unknown resource: ${uri}`) };
129
129
  }
130
+ console.log(`${logPrefix} resources/read ${uri}`);
131
+ const content = resource.read();
132
+ console.log(`${logPrefix} resources/read ${uri} done: ${JSON.stringify(content).slice(0, 200)}`);
130
133
  return {
131
134
  body: rpcResult(id, {
132
135
  contents: [{
133
136
  uri: resource.uri,
134
137
  mimeType: resource.mimeType,
135
- text: JSON.stringify(resource.read()),
138
+ text: JSON.stringify(content),
136
139
  }],
137
140
  }),
138
141
  };
package/dist/mcp-tools.js CHANGED
@@ -2,6 +2,7 @@ import { StringCodec } from "nats";
2
2
  import { registerPending } from "./pending-requests.js";
3
3
  import { getLocationDevice } from "./location-device.js";
4
4
  import { getNotifications } from "./notification-store.js";
5
+ import { getSmsMessages } from "./sms-store.js";
5
6
  export class ToolError extends Error {
6
7
  statusCode;
7
8
  constructor(message, statusCode = 500) {
@@ -171,7 +172,385 @@ const deviceGeolocationTool = {
171
172
  return locationData;
172
173
  },
173
174
  };
174
- export const agentTools = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool];
175
+ const readContactsTool = {
176
+ name: "read-contacts",
177
+ description: [
178
+ "Read the contact list from the user's mobile device.",
179
+ "Blocks until the device responds (up to 30 seconds).",
180
+ 'Response: `{"contacts": [{"id": ..., "name": ..., "phone": ...}]}` on success, or `{"error": "..."}` on failure.',
181
+ ],
182
+ inputSchema: {
183
+ type: "object",
184
+ properties: {},
185
+ },
186
+ async handler(_args, ctx) {
187
+ if (!ctx.nc)
188
+ throw new ToolError("Not connected to server (NATS unavailable)", 503);
189
+ const sc = StringCodec();
190
+ const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.contacts`, sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, action: "read" })), { timeout: 5_000 });
191
+ const ack = JSON.parse(sc.decode(ackReply.data));
192
+ if (ack.error)
193
+ throw new ToolError(ack.error, 502);
194
+ const responsePromise = new Promise((resolve, reject) => {
195
+ const sub = ctx.nc.subscribe(`host.${ctx.config.hostId}.contacts.${ctx.sessionId}`, { max: 1 });
196
+ const timer = setTimeout(() => {
197
+ sub.unsubscribe();
198
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
199
+ }, 30_000);
200
+ (async () => {
201
+ for await (const msg of sub) {
202
+ clearTimeout(timer);
203
+ resolve(sc.decode(msg.data));
204
+ }
205
+ })();
206
+ });
207
+ const result = JSON.parse(await responsePromise);
208
+ if (result.error)
209
+ return { error: result.error };
210
+ return result;
211
+ },
212
+ };
213
+ const createContactTool = {
214
+ name: "create-contact",
215
+ description: [
216
+ "Create a new contact on the user's mobile device.",
217
+ "Blocks until the device responds (up to 30 seconds).",
218
+ 'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
219
+ ],
220
+ inputSchema: {
221
+ type: "object",
222
+ properties: {
223
+ name: { type: "string", description: "Contact display name" },
224
+ phone: { type: "string", description: "Phone number" },
225
+ email: { type: "string", description: "Email address" },
226
+ },
227
+ required: ["name"],
228
+ },
229
+ async handler(args, ctx) {
230
+ if (!ctx.nc)
231
+ throw new ToolError("Not connected to server (NATS unavailable)", 503);
232
+ const { name, phone, email } = args;
233
+ if (!name)
234
+ throw new ToolError("name is required", 400);
235
+ const sc = StringCodec();
236
+ const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.contacts`, sc.encode(JSON.stringify({
237
+ hostId: ctx.config.hostId, requestId: ctx.sessionId,
238
+ action: "create", name, phone, email,
239
+ })), { timeout: 5_000 });
240
+ const ack = JSON.parse(sc.decode(ackReply.data));
241
+ if (ack.error)
242
+ throw new ToolError(ack.error, 502);
243
+ const responsePromise = new Promise((resolve, reject) => {
244
+ const sub = ctx.nc.subscribe(`host.${ctx.config.hostId}.contacts.${ctx.sessionId}`, { max: 1 });
245
+ const timer = setTimeout(() => {
246
+ sub.unsubscribe();
247
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
248
+ }, 30_000);
249
+ (async () => {
250
+ for await (const msg of sub) {
251
+ clearTimeout(timer);
252
+ resolve(sc.decode(msg.data));
253
+ }
254
+ })();
255
+ });
256
+ const result = JSON.parse(await responsePromise);
257
+ if (result.error)
258
+ return { error: result.error };
259
+ return result;
260
+ },
261
+ };
262
+ const readCalendarTool = {
263
+ name: "read-calendar",
264
+ description: [
265
+ "Read calendar events from the user's mobile device.",
266
+ "Blocks until the device responds (up to 30 seconds).",
267
+ "Pass startDate and endDate as Unix timestamps in milliseconds. Defaults to next 7 days.",
268
+ 'Response: `{"events": [{"id": ..., "title": ..., "startTime": ..., "endTime": ..., "location": ..., "description": ..., "allDay": ..., "calendar": ...}]}` on success.',
269
+ ],
270
+ inputSchema: {
271
+ type: "object",
272
+ properties: {
273
+ startDate: { type: "number", description: "Start of range (Unix ms). Defaults to now." },
274
+ endDate: { type: "number", description: "End of range (Unix ms). Defaults to 7 days from start." },
275
+ },
276
+ },
277
+ async handler(args, ctx) {
278
+ if (!ctx.nc)
279
+ throw new ToolError("Not connected to server (NATS unavailable)", 503);
280
+ const { startDate, endDate } = args;
281
+ const sc = StringCodec();
282
+ const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.calendar`, sc.encode(JSON.stringify({
283
+ hostId: ctx.config.hostId, requestId: ctx.sessionId,
284
+ action: "read",
285
+ ...(startDate ? { startDate: String(startDate) } : {}),
286
+ ...(endDate ? { endDate: String(endDate) } : {}),
287
+ })), { timeout: 5_000 });
288
+ const ack = JSON.parse(sc.decode(ackReply.data));
289
+ if (ack.error)
290
+ throw new ToolError(ack.error, 502);
291
+ const responsePromise = new Promise((resolve, reject) => {
292
+ const sub = ctx.nc.subscribe(`host.${ctx.config.hostId}.calendar.${ctx.sessionId}`, { max: 1 });
293
+ const timer = setTimeout(() => {
294
+ sub.unsubscribe();
295
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
296
+ }, 30_000);
297
+ (async () => {
298
+ for await (const msg of sub) {
299
+ clearTimeout(timer);
300
+ resolve(sc.decode(msg.data));
301
+ }
302
+ })();
303
+ });
304
+ const result = JSON.parse(await responsePromise);
305
+ if (result.error)
306
+ return { error: result.error };
307
+ return result;
308
+ },
309
+ };
310
+ const createCalendarEventTool = {
311
+ name: "create-calendar-event",
312
+ description: [
313
+ "Create a calendar event on the user's mobile device.",
314
+ "Blocks until the device responds (up to 30 seconds).",
315
+ 'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
316
+ ],
317
+ inputSchema: {
318
+ type: "object",
319
+ properties: {
320
+ title: { type: "string", description: "Event title" },
321
+ startTime: { type: "number", description: "Start time (Unix ms)" },
322
+ endTime: { type: "number", description: "End time (Unix ms)" },
323
+ location: { type: "string", description: "Event location" },
324
+ description: { type: "string", description: "Event description" },
325
+ },
326
+ required: ["title", "startTime", "endTime"],
327
+ },
328
+ async handler(args, ctx) {
329
+ if (!ctx.nc)
330
+ throw new ToolError("Not connected to server (NATS unavailable)", 503);
331
+ const { title, startTime, endTime, location, description } = args;
332
+ if (!title || !startTime || !endTime)
333
+ throw new ToolError("title, startTime, and endTime are required", 400);
334
+ const sc = StringCodec();
335
+ const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.calendar`, sc.encode(JSON.stringify({
336
+ hostId: ctx.config.hostId, requestId: ctx.sessionId,
337
+ action: "create",
338
+ title, startTime: String(startTime), endTime: String(endTime),
339
+ ...(location ? { location } : {}),
340
+ ...(description ? { description } : {}),
341
+ })), { timeout: 5_000 });
342
+ const ack = JSON.parse(sc.decode(ackReply.data));
343
+ if (ack.error)
344
+ throw new ToolError(ack.error, 502);
345
+ const responsePromise = new Promise((resolve, reject) => {
346
+ const sub = ctx.nc.subscribe(`host.${ctx.config.hostId}.calendar.${ctx.sessionId}`, { max: 1 });
347
+ const timer = setTimeout(() => {
348
+ sub.unsubscribe();
349
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
350
+ }, 30_000);
351
+ (async () => {
352
+ for await (const msg of sub) {
353
+ clearTimeout(timer);
354
+ resolve(sc.decode(msg.data));
355
+ }
356
+ })();
357
+ });
358
+ const result = JSON.parse(await responsePromise);
359
+ if (result.error)
360
+ return { error: result.error };
361
+ return result;
362
+ },
363
+ };
364
+ const sendSmsTool = {
365
+ name: "send-sms",
366
+ description: [
367
+ "Send an SMS message from the user's mobile device.",
368
+ "Blocks until the device responds (up to 30 seconds).",
369
+ 'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
370
+ ],
371
+ inputSchema: {
372
+ type: "object",
373
+ properties: {
374
+ to: { type: "string", description: "Recipient phone number" },
375
+ body: { type: "string", description: "Message text" },
376
+ },
377
+ required: ["to", "body"],
378
+ },
379
+ async handler(args, ctx) {
380
+ if (!ctx.nc)
381
+ throw new ToolError("Not connected to server (NATS unavailable)", 503);
382
+ const { to, body } = args;
383
+ if (!to || !body)
384
+ throw new ToolError("to and body are required", 400);
385
+ const sc = StringCodec();
386
+ const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.sms`, sc.encode(JSON.stringify({
387
+ hostId: ctx.config.hostId, requestId: ctx.sessionId,
388
+ action: "send", to, body,
389
+ })), { timeout: 5_000 });
390
+ const ack = JSON.parse(sc.decode(ackReply.data));
391
+ if (ack.error)
392
+ throw new ToolError(ack.error, 502);
393
+ const responsePromise = new Promise((resolve, reject) => {
394
+ const sub = ctx.nc.subscribe(`host.${ctx.config.hostId}.sms.${ctx.sessionId}`, { max: 1 });
395
+ const timer = setTimeout(() => {
396
+ sub.unsubscribe();
397
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
398
+ }, 30_000);
399
+ (async () => {
400
+ for await (const msg of sub) {
401
+ clearTimeout(timer);
402
+ resolve(sc.decode(msg.data));
403
+ }
404
+ })();
405
+ });
406
+ const result = JSON.parse(await responsePromise);
407
+ if (result.error)
408
+ return { error: result.error };
409
+ return result;
410
+ },
411
+ };
412
+ const setAlarmTool = {
413
+ name: "set-alarm",
414
+ description: [
415
+ "Set an alarm on the user's mobile device.",
416
+ "Blocks until the device responds (up to 30 seconds).",
417
+ 'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
418
+ ],
419
+ inputSchema: {
420
+ type: "object",
421
+ properties: {
422
+ hour: { type: "number", description: "Hour (0-23)" },
423
+ minutes: { type: "number", description: "Minutes (0-59)" },
424
+ label: { type: "string", description: "Alarm label" },
425
+ days: {
426
+ type: "array",
427
+ items: { type: "number" },
428
+ description: "Recurring days (1=Sun, 2=Mon, ..., 7=Sat). Omit for one-time.",
429
+ },
430
+ },
431
+ required: ["hour", "minutes"],
432
+ },
433
+ async handler(args, ctx) {
434
+ if (!ctx.nc)
435
+ throw new ToolError("Not connected to server (NATS unavailable)", 503);
436
+ const { hour, minutes, label, days } = args;
437
+ if (hour == null || minutes == null)
438
+ throw new ToolError("hour and minutes are required", 400);
439
+ const sc = StringCodec();
440
+ const payload = {
441
+ hostId: ctx.config.hostId, requestId: ctx.sessionId,
442
+ action: "set", hour: String(hour), minutes: String(minutes),
443
+ };
444
+ if (label)
445
+ payload.label = label;
446
+ if (days?.length)
447
+ payload.days = days.join(",");
448
+ const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.alarm`, sc.encode(JSON.stringify(payload)), { timeout: 5_000 });
449
+ const ack = JSON.parse(sc.decode(ackReply.data));
450
+ if (ack.error)
451
+ throw new ToolError(ack.error, 502);
452
+ const responsePromise = new Promise((resolve, reject) => {
453
+ const sub = ctx.nc.subscribe(`host.${ctx.config.hostId}.alarm.${ctx.sessionId}`, { max: 1 });
454
+ const timer = setTimeout(() => {
455
+ sub.unsubscribe();
456
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
457
+ }, 30_000);
458
+ (async () => {
459
+ for await (const msg of sub) {
460
+ clearTimeout(timer);
461
+ resolve(sc.decode(msg.data));
462
+ }
463
+ })();
464
+ });
465
+ const result = JSON.parse(await responsePromise);
466
+ if (result.error)
467
+ return { error: result.error };
468
+ return result;
469
+ },
470
+ };
471
+ const readBatteryTool = {
472
+ name: "read-battery",
473
+ description: [
474
+ "Get the battery level and charging status of the user's mobile device.",
475
+ "Blocks until the device responds (up to 30 seconds).",
476
+ 'Response: `{"level": 85, "charging": true}` on success, or `{"error": "..."}` on failure.',
477
+ ],
478
+ inputSchema: {
479
+ type: "object",
480
+ properties: {},
481
+ },
482
+ async handler(_args, ctx) {
483
+ if (!ctx.nc)
484
+ throw new ToolError("Not connected to server (NATS unavailable)", 503);
485
+ const sc = StringCodec();
486
+ const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.battery`, sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId })), { timeout: 5_000 });
487
+ const ack = JSON.parse(sc.decode(ackReply.data));
488
+ if (ack.error)
489
+ throw new ToolError(ack.error, 502);
490
+ const responsePromise = new Promise((resolve, reject) => {
491
+ const sub = ctx.nc.subscribe(`host.${ctx.config.hostId}.battery.${ctx.sessionId}`, { max: 1 });
492
+ const timer = setTimeout(() => {
493
+ sub.unsubscribe();
494
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
495
+ }, 30_000);
496
+ (async () => {
497
+ for await (const msg of sub) {
498
+ clearTimeout(timer);
499
+ resolve(sc.decode(msg.data));
500
+ }
501
+ })();
502
+ });
503
+ const result = JSON.parse(await responsePromise);
504
+ if (result.error)
505
+ return { error: result.error };
506
+ return result;
507
+ },
508
+ };
509
+ const setRingerModeTool = {
510
+ name: "set-ringer-mode",
511
+ description: [
512
+ "Set the phone's ringer mode. Requires Do Not Disturb access on the device.",
513
+ "Blocks until the device responds (up to 30 seconds).",
514
+ 'Response: `{"ok": true, "mode": "silent"}` on success, or `{"error": "..."}` on failure.',
515
+ ],
516
+ inputSchema: {
517
+ type: "object",
518
+ properties: {
519
+ mode: { type: "string", description: "Ringer mode: 'normal', 'vibrate', or 'silent'" },
520
+ },
521
+ required: ["mode"],
522
+ },
523
+ async handler(args, ctx) {
524
+ if (!ctx.nc)
525
+ throw new ToolError("Not connected to server (NATS unavailable)", 503);
526
+ const { mode } = args;
527
+ if (!["normal", "vibrate", "silent"].includes(mode))
528
+ throw new ToolError("mode must be 'normal', 'vibrate', or 'silent'", 400);
529
+ const sc = StringCodec();
530
+ const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.ringer`, sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, mode })), { timeout: 5_000 });
531
+ const ack = JSON.parse(sc.decode(ackReply.data));
532
+ if (ack.error)
533
+ throw new ToolError(ack.error, 502);
534
+ const responsePromise = new Promise((resolve, reject) => {
535
+ const sub = ctx.nc.subscribe(`host.${ctx.config.hostId}.ringer.${ctx.sessionId}`, { max: 1 });
536
+ const timer = setTimeout(() => {
537
+ sub.unsubscribe();
538
+ reject(new ToolError("Device did not respond within 30 seconds", 504));
539
+ }, 30_000);
540
+ (async () => {
541
+ for await (const msg of sub) {
542
+ clearTimeout(timer);
543
+ resolve(sc.decode(msg.data));
544
+ }
545
+ })();
546
+ });
547
+ const result = JSON.parse(await responsePromise);
548
+ if (result.error)
549
+ return { error: result.error };
550
+ return result;
551
+ },
552
+ };
553
+ export const agentTools = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, setAlarmTool, readBatteryTool, setRingerModeTool];
175
554
  export const agentToolMap = new Map(agentTools.map((t) => [t.name, t]));
176
555
  const deviceNotificationsResource = {
177
556
  uri: "notifications://device",
@@ -184,7 +563,18 @@ const deviceNotificationsResource = {
184
563
  restPath: "/notifications",
185
564
  read: getNotifications,
186
565
  };
187
- export const agentResources = [deviceNotificationsResource];
566
+ const deviceSmsResource = {
567
+ uri: "sms://device",
568
+ name: "Device SMS",
569
+ description: [
570
+ "Get recent SMS messages from the user's Android device.",
571
+ "Response: JSON array of message objects with `id`, `sender`, `body`, `timestamp`.",
572
+ ],
573
+ mimeType: "application/json",
574
+ restPath: "/sms",
575
+ read: getSmsMessages,
576
+ };
577
+ export const agentResources = [deviceNotificationsResource, deviceSmsResource];
188
578
  export const agentResourceMap = new Map(agentResources.map((r) => [r.uri, r]));
189
579
  /**
190
580
  * Generate the HTTP Endpoints markdown section for agent-instructions.md from the tool registry.
@@ -229,7 +619,7 @@ export function generateEndpointDocs(port, taskId, tools = agentTools, resources
229
619
  }
230
620
  for (const resource of resources) {
231
621
  const [header, ...details] = resource.description;
232
- lines.push(`**\`GET ${baseUrl}${resource.restPath}\`** — ${header}`);
622
+ lines.push(`**\`GET ${baseUrl}${resource.restPath}?taskId=${taskId}\`** — ${header}`);
233
623
  for (const detail of details) {
234
624
  lines.push(`- ${detail}`);
235
625
  }