palmier 0.6.9 → 0.7.0
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 +9 -2
- package/dist/commands/serve.js +16 -0
- package/dist/mcp-handler.d.ts +3 -0
- package/dist/mcp-handler.js +59 -3
- package/dist/mcp-tools.d.ts +16 -1
- package/dist/mcp-tools.js +23 -1
- package/dist/notification-store.d.ts +13 -0
- package/dist/notification-store.js +19 -0
- package/dist/pwa/assets/{index-CZejk2al.js → index-DLxrL0hR.js} +42 -42
- package/dist/pwa/assets/{web-C48txJFl.js → web-CBI458eN.js} +1 -1
- package/dist/pwa/assets/{web-zj8Blync.js → web-HDs03L2B.js} +1 -1
- package/dist/pwa/index.html +1 -1
- package/dist/pwa/service-worker.js +1 -1
- package/dist/transports/http-transport.js +51 -3
- package/package.json +1 -1
- package/palmier-server/README.md +1 -1
- package/palmier-server/pwa/src/components/PlanDialog.tsx +5 -12
- package/palmier-server/pwa/src/components/TaskForm.tsx +6 -15
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/types.ts +0 -1
- package/palmier-server/server/src/index.ts +2 -0
- package/palmier-server/server/src/routes/device.ts +32 -0
- package/palmier-server/spec.md +13 -12
- package/src/commands/serve.ts +16 -1
- package/src/mcp-handler.ts +68 -3
- package/src/mcp-tools.ts +47 -1
- package/src/notification-store.ts +30 -0
- package/src/transports/http-transport.ts +49 -3
- package/test/agent-instructions.test.ts +36 -3
- package/test/notification-store.test.ts +57 -0
package/README.md
CHANGED
|
@@ -34,11 +34,11 @@ 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,
|
|
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.
|
|
38
38
|
|
|
39
39
|
### MCP Server
|
|
40
40
|
|
|
41
|
-
Palmier exposes an [MCP](https://modelcontextprotocol.io) server at `http://localhost:<port>/mcp` (streamable HTTP transport). MCP-capable agents can register it to get tool definitions automatically. The same tools are also available as REST endpoints for curl-based agents.
|
|
41
|
+
Palmier exposes an [MCP](https://modelcontextprotocol.io) server at `http://localhost:<port>/mcp` (streamable HTTP transport). MCP-capable agents can register it to get tool and resource definitions automatically. The same tools and resources are also available as REST endpoints for curl-based agents.
|
|
42
42
|
|
|
43
43
|
**MCP server URL:** `http://localhost:<port>/mcp`
|
|
44
44
|
|
|
@@ -50,6 +50,13 @@ Palmier exposes an [MCP](https://modelcontextprotocol.io) server at `http://loca
|
|
|
50
50
|
| `request-confirmation` | Request confirmation from the user (blocks until response) |
|
|
51
51
|
| `device-geolocation` | Get GPS location of the user's mobile device |
|
|
52
52
|
|
|
53
|
+
**Available resources:**
|
|
54
|
+
| Resource | URI | REST | Description |
|
|
55
|
+
|----------|-----|------|-------------|
|
|
56
|
+
| Device Notifications | `notifications://device` | `GET /notifications` | Recent notifications from the user's Android device |
|
|
57
|
+
|
|
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.
|
|
59
|
+
|
|
53
60
|
```
|
|
54
61
|
┌──────────────┐ HTTP ┌──────────────────┐
|
|
55
62
|
│ │◄──────────────────────│ │
|
package/dist/commands/serve.js
CHANGED
|
@@ -11,6 +11,8 @@ import { getPlatform } from "../platform/index.js";
|
|
|
11
11
|
import { detectAgents } from "../agents/agent.js";
|
|
12
12
|
import { saveConfig } from "../config.js";
|
|
13
13
|
import { CONFIG_DIR } from "../config.js";
|
|
14
|
+
import { StringCodec } from "nats";
|
|
15
|
+
import { addNotification } from "../notification-store.js";
|
|
14
16
|
const POLL_INTERVAL_MS = 30_000;
|
|
15
17
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
16
18
|
/**
|
|
@@ -114,6 +116,20 @@ export async function serveCommand() {
|
|
|
114
116
|
// Start NATS transport (loops forever, fire-and-forget)
|
|
115
117
|
if (nc) {
|
|
116
118
|
startNatsTransport(config, handleRpc, nc);
|
|
119
|
+
// Subscribe to device notifications from Android
|
|
120
|
+
const sc = StringCodec();
|
|
121
|
+
const notifSub = nc.subscribe(`host.${config.hostId}.device.notifications`);
|
|
122
|
+
(async () => {
|
|
123
|
+
for await (const msg of notifSub) {
|
|
124
|
+
try {
|
|
125
|
+
const data = JSON.parse(sc.decode(msg.data));
|
|
126
|
+
addNotification({ ...data, receivedAt: Date.now() });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
console.error("[nats] Failed to parse device notification:", err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
117
133
|
}
|
|
118
134
|
// Start HTTP transport (loops forever)
|
|
119
135
|
await startHttpTransport(config, handleRpc, httpPort, nc);
|
package/dist/mcp-handler.d.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { type ToolContext } from "./mcp-tools.js";
|
|
|
2
2
|
export interface McpResponse {
|
|
3
3
|
body: object;
|
|
4
4
|
sessionId?: string;
|
|
5
|
+
/** If true, the HTTP transport should keep the response open as an SSE stream for server-initiated notifications. */
|
|
6
|
+
stream?: boolean;
|
|
5
7
|
}
|
|
8
|
+
export declare function getResourceSubscriptions(): Map<string, Set<string>>;
|
|
6
9
|
export declare function getAgentName(sessionId: string): string | undefined;
|
|
7
10
|
export declare function handleMcpRequest(body: string, sessionId: string | undefined, ctx: ToolContext): Promise<McpResponse>;
|
|
8
11
|
//# sourceMappingURL=mcp-handler.d.ts.map
|
package/dist/mcp-handler.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
-
import { agentTools, agentToolMap, ToolError } from "./mcp-tools.js";
|
|
2
|
+
import { agentTools, agentToolMap, agentResources, agentResourceMap, ToolError } from "./mcp-tools.js";
|
|
3
|
+
// Resource subscriptions: sessionId → Set of resource URIs
|
|
4
|
+
const resourceSubscriptions = new Map();
|
|
5
|
+
export function getResourceSubscriptions() {
|
|
6
|
+
return resourceSubscriptions;
|
|
7
|
+
}
|
|
3
8
|
// Session-to-agent name map with 24h TTL
|
|
4
9
|
const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
5
10
|
const sessionAgents = new Map();
|
|
@@ -16,8 +21,10 @@ export function getAgentName(sessionId) {
|
|
|
16
21
|
function pruneExpiredSessions() {
|
|
17
22
|
const now = Date.now();
|
|
18
23
|
for (const [id, entry] of sessionAgents) {
|
|
19
|
-
if (now > entry.expiresAt)
|
|
24
|
+
if (now > entry.expiresAt) {
|
|
20
25
|
sessionAgents.delete(id);
|
|
26
|
+
resourceSubscriptions.delete(id);
|
|
27
|
+
}
|
|
21
28
|
}
|
|
22
29
|
}
|
|
23
30
|
function rpcError(id, code, message) {
|
|
@@ -57,7 +64,7 @@ export async function handleMcpRequest(body, sessionId, ctx) {
|
|
|
57
64
|
return {
|
|
58
65
|
body: rpcResult(id, {
|
|
59
66
|
protocolVersion: "2025-03-26",
|
|
60
|
-
capabilities: { tools: {} },
|
|
67
|
+
capabilities: { tools: {}, resources: { subscribe: true } },
|
|
61
68
|
serverInfo: { name: "palmier", version: "1.0.0" },
|
|
62
69
|
}),
|
|
63
70
|
sessionId: newSessionId,
|
|
@@ -102,6 +109,55 @@ export async function handleMcpRequest(body, sessionId, ctx) {
|
|
|
102
109
|
};
|
|
103
110
|
}
|
|
104
111
|
}
|
|
112
|
+
case "resources/list": {
|
|
113
|
+
return {
|
|
114
|
+
body: rpcResult(id, {
|
|
115
|
+
resources: agentResources.map((r) => ({
|
|
116
|
+
uri: r.uri,
|
|
117
|
+
name: r.name,
|
|
118
|
+
description: r.description[0],
|
|
119
|
+
mimeType: r.mimeType,
|
|
120
|
+
})),
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
case "resources/read": {
|
|
125
|
+
const uri = req.params?.uri;
|
|
126
|
+
const resource = agentResourceMap.get(uri);
|
|
127
|
+
if (!resource) {
|
|
128
|
+
return { body: rpcError(id, -32602, `Unknown resource: ${uri}`) };
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
body: rpcResult(id, {
|
|
132
|
+
contents: [{
|
|
133
|
+
uri: resource.uri,
|
|
134
|
+
mimeType: resource.mimeType,
|
|
135
|
+
text: JSON.stringify(resource.read()),
|
|
136
|
+
}],
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
case "resources/subscribe": {
|
|
141
|
+
const uri = req.params?.uri;
|
|
142
|
+
if (!agentResourceMap.has(uri)) {
|
|
143
|
+
return { body: rpcError(id, -32602, `Unknown resource: ${uri}`) };
|
|
144
|
+
}
|
|
145
|
+
if (!sessionId) {
|
|
146
|
+
return { body: rpcError(id, -32600, "Session required for subscriptions") };
|
|
147
|
+
}
|
|
148
|
+
if (!resourceSubscriptions.has(sessionId)) {
|
|
149
|
+
resourceSubscriptions.set(sessionId, new Set());
|
|
150
|
+
}
|
|
151
|
+
resourceSubscriptions.get(sessionId).add(uri);
|
|
152
|
+
return { body: rpcResult(id, {}), stream: true };
|
|
153
|
+
}
|
|
154
|
+
case "resources/unsubscribe": {
|
|
155
|
+
const uri = req.params?.uri;
|
|
156
|
+
if (sessionId) {
|
|
157
|
+
resourceSubscriptions.get(sessionId)?.delete(uri);
|
|
158
|
+
}
|
|
159
|
+
return { body: rpcResult(id, {}) };
|
|
160
|
+
}
|
|
105
161
|
default:
|
|
106
162
|
console.warn(`${logPrefix} Unknown method: ${req.method}`);
|
|
107
163
|
return { body: rpcError(id, -32601, `Method not found: ${req.method}`) };
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -20,8 +20,23 @@ export interface ToolDefinition {
|
|
|
20
20
|
}
|
|
21
21
|
export declare const agentTools: ToolDefinition[];
|
|
22
22
|
export declare const agentToolMap: Map<string, ToolDefinition>;
|
|
23
|
+
export interface ResourceDefinition {
|
|
24
|
+
/** MCP resource URI (e.g. "notifications://device"). */
|
|
25
|
+
uri: string;
|
|
26
|
+
/** Display name. */
|
|
27
|
+
name: string;
|
|
28
|
+
/** First line is the summary (used as REST endpoint header). Remaining lines become bullet points in docs. */
|
|
29
|
+
description: string[];
|
|
30
|
+
mimeType: string;
|
|
31
|
+
/** REST endpoint path (e.g. "/notifications"). Served as GET. */
|
|
32
|
+
restPath: string;
|
|
33
|
+
/** Return the current resource content. */
|
|
34
|
+
read: () => unknown;
|
|
35
|
+
}
|
|
36
|
+
export declare const agentResources: ResourceDefinition[];
|
|
37
|
+
export declare const agentResourceMap: Map<string, ResourceDefinition>;
|
|
23
38
|
/**
|
|
24
39
|
* Generate the HTTP Endpoints markdown section for agent-instructions.md from the tool registry.
|
|
25
40
|
*/
|
|
26
|
-
export declare function generateEndpointDocs(port: number, taskId: string, tools?: ToolDefinition[]): string;
|
|
41
|
+
export declare function generateEndpointDocs(port: number, taskId: string, tools?: ToolDefinition[], resources?: ResourceDefinition[]): string;
|
|
27
42
|
//# sourceMappingURL=mcp-tools.d.ts.map
|
package/dist/mcp-tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StringCodec } from "nats";
|
|
2
2
|
import { registerPending } from "./pending-requests.js";
|
|
3
3
|
import { getLocationDevice } from "./location-device.js";
|
|
4
|
+
import { getNotifications } from "./notification-store.js";
|
|
4
5
|
export class ToolError extends Error {
|
|
5
6
|
statusCode;
|
|
6
7
|
constructor(message, statusCode = 500) {
|
|
@@ -172,10 +173,23 @@ const deviceGeolocationTool = {
|
|
|
172
173
|
};
|
|
173
174
|
export const agentTools = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool];
|
|
174
175
|
export const agentToolMap = new Map(agentTools.map((t) => [t.name, t]));
|
|
176
|
+
const deviceNotificationsResource = {
|
|
177
|
+
uri: "notifications://device",
|
|
178
|
+
name: "Device Notifications",
|
|
179
|
+
description: [
|
|
180
|
+
"Get recent notifications from the user's Android device.",
|
|
181
|
+
"Response: JSON array of notification objects with `id`, `packageName`, `appName`, `title`, `text`, `timestamp`.",
|
|
182
|
+
],
|
|
183
|
+
mimeType: "application/json",
|
|
184
|
+
restPath: "/notifications",
|
|
185
|
+
read: getNotifications,
|
|
186
|
+
};
|
|
187
|
+
export const agentResources = [deviceNotificationsResource];
|
|
188
|
+
export const agentResourceMap = new Map(agentResources.map((r) => [r.uri, r]));
|
|
175
189
|
/**
|
|
176
190
|
* Generate the HTTP Endpoints markdown section for agent-instructions.md from the tool registry.
|
|
177
191
|
*/
|
|
178
|
-
export function generateEndpointDocs(port, taskId, tools = agentTools) {
|
|
192
|
+
export function generateEndpointDocs(port, taskId, tools = agentTools, resources = agentResources) {
|
|
179
193
|
const baseUrl = `http://localhost:${port}`;
|
|
180
194
|
const lines = [
|
|
181
195
|
`The following HTTP endpoints are available during task execution. Use curl to call them.`,
|
|
@@ -213,6 +227,14 @@ export function generateEndpointDocs(port, taskId, tools = agentTools) {
|
|
|
213
227
|
}
|
|
214
228
|
lines.push("");
|
|
215
229
|
}
|
|
230
|
+
for (const resource of resources) {
|
|
231
|
+
const [header, ...details] = resource.description;
|
|
232
|
+
lines.push(`**\`GET ${baseUrl}${resource.restPath}\`** — ${header}`);
|
|
233
|
+
for (const detail of details) {
|
|
234
|
+
lines.push(`- ${detail}`);
|
|
235
|
+
}
|
|
236
|
+
lines.push("");
|
|
237
|
+
}
|
|
216
238
|
return lines.join("\n").trimEnd();
|
|
217
239
|
}
|
|
218
240
|
//# sourceMappingURL=mcp-tools.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface DeviceNotification {
|
|
2
|
+
id: string;
|
|
3
|
+
packageName: string;
|
|
4
|
+
appName: string;
|
|
5
|
+
title: string;
|
|
6
|
+
text: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
receivedAt: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function addNotification(n: DeviceNotification): void;
|
|
11
|
+
export declare function getNotifications(): DeviceNotification[];
|
|
12
|
+
export declare function onNotificationsChanged(cb: () => void): () => void;
|
|
13
|
+
//# sourceMappingURL=notification-store.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const MAX_NOTIFICATIONS = 50;
|
|
2
|
+
const notifications = [];
|
|
3
|
+
const listeners = new Set();
|
|
4
|
+
export function addNotification(n) {
|
|
5
|
+
notifications.push(n);
|
|
6
|
+
if (notifications.length > MAX_NOTIFICATIONS) {
|
|
7
|
+
notifications.shift();
|
|
8
|
+
}
|
|
9
|
+
for (const cb of listeners)
|
|
10
|
+
cb();
|
|
11
|
+
}
|
|
12
|
+
export function getNotifications() {
|
|
13
|
+
return [...notifications];
|
|
14
|
+
}
|
|
15
|
+
export function onNotificationsChanged(cb) {
|
|
16
|
+
listeners.add(cb);
|
|
17
|
+
return () => { listeners.delete(cb); };
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=notification-store.js.map
|