palmier 0.7.6 → 0.7.7
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/dist/agents/shared-prompt.js +1 -1
- package/dist/commands/init.js +3 -2
- package/dist/commands/pair.js +1 -1
- package/dist/commands/run.js +4 -4
- package/dist/commands/serve.js +1 -1
- package/dist/config.js +2 -2
- package/dist/device-capabilities.d.ts +1 -1
- package/dist/events.js +1 -1
- package/dist/mcp-tools.js +64 -1
- package/dist/nats-client.d.ts +1 -1
- package/dist/nats-client.js +6 -3
- package/dist/pwa/assets/index-Bt8Hhaw3.js +118 -0
- package/dist/pwa/assets/{web-DnuoxUd4.js → web-CkWrlNwc.js} +1 -1
- package/dist/pwa/assets/{web-7raT3zOZ.js → web-lx34oBi7.js} +1 -1
- package/dist/pwa/index.html +1 -1
- package/dist/pwa/service-worker.js +1 -1
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
- package/palmier-server/PRODUCTION.md +31 -28
- package/palmier-server/README.md +35 -5
- package/palmier-server/nats.conf +9 -5
- package/palmier-server/package.json +2 -1
- package/palmier-server/pnpm-lock.yaml +6 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +58 -0
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +9 -5
- package/palmier-server/pwa/src/pages/PairHost.tsx +6 -3
- package/palmier-server/server/package.json +3 -1
- package/palmier-server/server/src/index.ts +83 -2
- package/palmier-server/server/src/nats-jwt.ts +299 -0
- package/palmier-server/server/src/nats-setup.ts +48 -0
- package/palmier-server/server/src/nats.ts +12 -4
- package/palmier-server/server/src/routes/device.ts +24 -0
- package/palmier-server/server/src/routes/hosts.ts +13 -2
- package/palmier-server/spec.md +6 -5
- package/src/agents/shared-prompt.ts +1 -1
- package/src/commands/init.ts +7 -5
- package/src/commands/pair.ts +1 -1
- package/src/commands/run.ts +4 -4
- package/src/commands/serve.ts +1 -1
- package/src/config.ts +2 -2
- package/src/device-capabilities.ts +1 -0
- package/src/events.ts +1 -1
- package/src/mcp-tools.ts +68 -1
- package/src/nats-client.ts +10 -3
- package/src/types.ts +3 -2
- package/test/agent-instructions.test.ts +10 -10
- package/dist/pwa/assets/index-uSwkmHBs.js +0 -118
package/src/commands/run.ts
CHANGED
|
@@ -67,7 +67,7 @@ async function invokeAgentWithRetries(
|
|
|
67
67
|
);
|
|
68
68
|
const result = await spawnCommand(command, args, {
|
|
69
69
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
70
|
-
env: { ...ctx.guiEnv, ...agentEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ??
|
|
70
|
+
env: { ...ctx.guiEnv, ...agentEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7256) },
|
|
71
71
|
echoStdout: true,
|
|
72
72
|
resolveOnFailure: true,
|
|
73
73
|
stdin,
|
|
@@ -321,7 +321,7 @@ async function runCommandTriggeredMode(
|
|
|
321
321
|
|
|
322
322
|
const child = spawnStreamingCommand(commandStr, {
|
|
323
323
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
324
|
-
env: { ...ctx.guiEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ??
|
|
324
|
+
env: { ...ctx.guiEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7256) },
|
|
325
325
|
});
|
|
326
326
|
|
|
327
327
|
let linesProcessed = 0;
|
|
@@ -483,7 +483,7 @@ async function requestPermission(
|
|
|
483
483
|
taskDir: string,
|
|
484
484
|
requiredPermissions: RequiredPermission[],
|
|
485
485
|
): Promise<"granted" | "granted_all" | "aborted"> {
|
|
486
|
-
const port = config.httpPort ??
|
|
486
|
+
const port = config.httpPort ?? 7256;
|
|
487
487
|
const res = await fetch(`http://localhost:${port}/request-permission`, {
|
|
488
488
|
method: "POST",
|
|
489
489
|
headers: { "Content-Type": "application/json" },
|
|
@@ -511,7 +511,7 @@ async function requestConfirmation(
|
|
|
511
511
|
task: ParsedTask,
|
|
512
512
|
taskDir: string,
|
|
513
513
|
): Promise<boolean> {
|
|
514
|
-
const port = config.httpPort ??
|
|
514
|
+
const port = config.httpPort ?? 7256;
|
|
515
515
|
const res = await fetch(`http://localhost:${port}/request-confirmation?taskId=${encodeURIComponent(task.frontmatter.id)}`, {
|
|
516
516
|
method: "POST",
|
|
517
517
|
headers: { "Content-Type": "application/json" },
|
package/src/commands/serve.ts
CHANGED
|
@@ -127,7 +127,7 @@ export async function serveCommand(): Promise<void> {
|
|
|
127
127
|
}, POLL_INTERVAL_MS);
|
|
128
128
|
|
|
129
129
|
const handleRpc = createRpcHandler(config, nc);
|
|
130
|
-
const httpPort = config.httpPort ??
|
|
130
|
+
const httpPort = config.httpPort ?? 7256;
|
|
131
131
|
|
|
132
132
|
// Start NATS transport (loops forever, fire-and-forget)
|
|
133
133
|
if (nc) {
|
package/src/config.ts
CHANGED
|
@@ -25,8 +25,8 @@ export function loadConfig(): HostConfig {
|
|
|
25
25
|
throw new Error("Invalid host config: missing hostId");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
if (!config.natsUrl || !config.
|
|
29
|
-
throw new Error("Invalid host config: missing
|
|
28
|
+
if (!config.natsUrl || !config.natsJwt || !config.natsNkeySeed) {
|
|
29
|
+
throw new Error("Invalid host config: missing NATS JWT credentials. Re-run palmier init.");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
return config;
|
package/src/events.ts
CHANGED
package/src/mcp-tools.ts
CHANGED
|
@@ -657,7 +657,74 @@ const setRingerModeTool: ToolDefinition = {
|
|
|
657
657
|
},
|
|
658
658
|
};
|
|
659
659
|
|
|
660
|
-
|
|
660
|
+
const sendEmailTool: ToolDefinition = {
|
|
661
|
+
name: "send-email",
|
|
662
|
+
description: [
|
|
663
|
+
"Send an email from the user's mobile device.",
|
|
664
|
+
"When you need to send an email, use this tool. The email app opens on the device with the draft pre-filled for the user to review and send.",
|
|
665
|
+
'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
|
|
666
|
+
],
|
|
667
|
+
inputSchema: {
|
|
668
|
+
type: "object",
|
|
669
|
+
properties: {
|
|
670
|
+
to: { type: "string", description: "Recipient email address" },
|
|
671
|
+
subject: { type: "string", description: "Email subject" },
|
|
672
|
+
body: { type: "string", description: "Email body text" },
|
|
673
|
+
cc: { type: "string", description: "CC recipient(s)" },
|
|
674
|
+
bcc: { type: "string", description: "BCC recipient(s)" },
|
|
675
|
+
},
|
|
676
|
+
required: ["to"],
|
|
677
|
+
},
|
|
678
|
+
async handler(args, ctx) {
|
|
679
|
+
if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
680
|
+
|
|
681
|
+
const device = getCapabilityDevice("email");
|
|
682
|
+
if (!device) throw new ToolError("No device has email access enabled", 400);
|
|
683
|
+
|
|
684
|
+
const { to, subject, body, cc, bcc } = args as { to: string; subject?: string; body?: string; cc?: string; bcc?: string };
|
|
685
|
+
if (!to) throw new ToolError("to is required", 400);
|
|
686
|
+
|
|
687
|
+
const sc = StringCodec();
|
|
688
|
+
|
|
689
|
+
const payload: Record<string, string> = {
|
|
690
|
+
hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken,
|
|
691
|
+
to,
|
|
692
|
+
};
|
|
693
|
+
if (subject) payload.subject = subject;
|
|
694
|
+
if (body) payload.body = body;
|
|
695
|
+
if (cc) payload.cc = cc;
|
|
696
|
+
if (bcc) payload.bcc = bcc;
|
|
697
|
+
|
|
698
|
+
const ackReply = await ctx.nc.request(
|
|
699
|
+
`host.${ctx.config.hostId}.fcm.email`,
|
|
700
|
+
sc.encode(JSON.stringify(payload)),
|
|
701
|
+
{ timeout: 5_000 },
|
|
702
|
+
);
|
|
703
|
+
const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
|
|
704
|
+
if (ack.error) throw new ToolError(ack.error, 502);
|
|
705
|
+
|
|
706
|
+
const responsePromise = new Promise<string>((resolve, reject) => {
|
|
707
|
+
const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.email.${ctx.sessionId}`, { max: 1 });
|
|
708
|
+
const timer = setTimeout(() => {
|
|
709
|
+
sub.unsubscribe();
|
|
710
|
+
reject(new ToolError("Device did not respond within 30 seconds", 504));
|
|
711
|
+
}, 30_000);
|
|
712
|
+
|
|
713
|
+
(async () => {
|
|
714
|
+
for await (const msg of sub) {
|
|
715
|
+
clearTimeout(timer);
|
|
716
|
+
resolve(sc.decode(msg.data));
|
|
717
|
+
}
|
|
718
|
+
})();
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const result = JSON.parse(await responsePromise);
|
|
722
|
+
if (result.error) return { error: result.error };
|
|
723
|
+
return result;
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlertTool, readBatteryTool, setRingerModeTool];
|
|
661
728
|
export const agentToolMap = new Map<string, ToolDefinition>(agentTools.map((t) => [t.name, t]));
|
|
662
729
|
|
|
663
730
|
// ── MCP Resources ─────────────────────────────────────────────────────
|
package/src/nats-client.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import { connect, type NatsConnection } from "nats";
|
|
1
|
+
import { connect, jwtAuthenticator, type NatsConnection } from "nats";
|
|
2
2
|
import type { HostConfig } from "./types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Connect to NATS using the host config's
|
|
5
|
+
* Connect to NATS using the host config's JWT credentials.
|
|
6
6
|
*/
|
|
7
7
|
export async function connectNats(config: HostConfig): Promise<NatsConnection> {
|
|
8
|
+
if (!config.natsJwt || !config.natsNkeySeed) {
|
|
9
|
+
throw new Error("NATS JWT credentials not configured. Re-run palmier init.");
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
const nc = await connect({
|
|
9
13
|
servers: config.natsUrl,
|
|
10
|
-
|
|
14
|
+
authenticator: jwtAuthenticator(
|
|
15
|
+
config.natsJwt,
|
|
16
|
+
new TextEncoder().encode(config.natsNkeySeed),
|
|
17
|
+
),
|
|
11
18
|
});
|
|
12
19
|
|
|
13
20
|
// Do not log anything as that will pollute stdout for mcp server.
|
package/src/types.ts
CHANGED
|
@@ -4,12 +4,13 @@ export interface HostConfig {
|
|
|
4
4
|
|
|
5
5
|
natsUrl?: string;
|
|
6
6
|
natsWsUrl?: string;
|
|
7
|
-
|
|
7
|
+
natsJwt?: string;
|
|
8
|
+
natsNkeySeed?: string;
|
|
8
9
|
|
|
9
10
|
// Detected agent CLIs
|
|
10
11
|
agents?: Array<{ key: string; label: string }>;
|
|
11
12
|
|
|
12
|
-
// HTTP server port (default
|
|
13
|
+
// HTTP server port (default 7256)
|
|
13
14
|
httpPort?: number;
|
|
14
15
|
// Whether to accept non-localhost HTTP connections
|
|
15
16
|
lanEnabled?: boolean;
|
|
@@ -66,7 +66,7 @@ const mockResources: ResourceDefinition[] = [
|
|
|
66
66
|
/** Minimal replica of getAgentInstructions that doesn't need host.json */
|
|
67
67
|
function buildInstructions(taskId: string, opts?: { skipPermissions?: boolean }): string {
|
|
68
68
|
let instructions = template
|
|
69
|
-
.replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(
|
|
69
|
+
.replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(7256, taskId, mockTools, mockResources))
|
|
70
70
|
.replace(/\{\{TASK_DESCRIPTION\}\}/g, "Test task prompt");
|
|
71
71
|
if (opts?.skipPermissions) {
|
|
72
72
|
instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
|
|
@@ -107,7 +107,7 @@ describe("getAgentInstructions", () => {
|
|
|
107
107
|
|
|
108
108
|
it("includes port in endpoint URL", () => {
|
|
109
109
|
const result = buildInstructions("test");
|
|
110
|
-
assert.match(result, /localhost:
|
|
110
|
+
assert.match(result, /localhost:7256/);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
it("includes task description", () => {
|
|
@@ -119,13 +119,13 @@ describe("getAgentInstructions", () => {
|
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
describe("generateEndpointDocs", () => {
|
|
122
|
-
const docs = generateEndpointDocs(
|
|
122
|
+
const docs = generateEndpointDocs(7256, "test-id", mockTools, mockResources);
|
|
123
123
|
|
|
124
124
|
it("matches expected full output", () => {
|
|
125
125
|
const expected = [
|
|
126
126
|
"The following HTTP endpoints are available during task execution. Use curl to call them.",
|
|
127
127
|
"",
|
|
128
|
-
"**`POST http://localhost:
|
|
128
|
+
"**`POST http://localhost:7256/mock-action?taskId=test-id`** — Perform a mock action.",
|
|
129
129
|
"```json",
|
|
130
130
|
'{"title":"...","detail":"..."}',
|
|
131
131
|
"```",
|
|
@@ -133,7 +133,7 @@ describe("generateEndpointDocs", () => {
|
|
|
133
133
|
"- `detail` (optional, string): Optional detail",
|
|
134
134
|
'- Response: `{"ok": true}` on success.',
|
|
135
135
|
"",
|
|
136
|
-
"**`POST http://localhost:
|
|
136
|
+
"**`POST http://localhost:7256/mock-query?taskId=test-id`** — Query mock data from the device.",
|
|
137
137
|
"```json",
|
|
138
138
|
'{"tags":["..."]}',
|
|
139
139
|
"```",
|
|
@@ -141,7 +141,7 @@ describe("generateEndpointDocs", () => {
|
|
|
141
141
|
"- Blocks until the device responds.",
|
|
142
142
|
'- Response: `{"data": ...}` on success.',
|
|
143
143
|
"",
|
|
144
|
-
"**`GET http://localhost:
|
|
144
|
+
"**`GET http://localhost:7256/mock-data?taskId=test-id`** — Get mock data from the device.",
|
|
145
145
|
"- Response: JSON array of data objects.",
|
|
146
146
|
].join("\n");
|
|
147
147
|
assert.equal(docs, expected);
|
|
@@ -149,7 +149,7 @@ describe("generateEndpointDocs", () => {
|
|
|
149
149
|
|
|
150
150
|
it("generates docs for all provided tools", () => {
|
|
151
151
|
for (const tool of mockTools) {
|
|
152
|
-
assert.match(docs, new RegExp(`POST http://localhost:
|
|
152
|
+
assert.match(docs, new RegExp(`POST http://localhost:7256/${tool.name}\\?taskId=`), `Missing endpoint for ${tool.name}`);
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
155
|
|
|
@@ -162,7 +162,7 @@ describe("generateEndpointDocs", () => {
|
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
it("includes port in the header", () => {
|
|
165
|
-
assert.match(docs, /localhost:
|
|
165
|
+
assert.match(docs, /localhost:7256/);
|
|
166
166
|
});
|
|
167
167
|
|
|
168
168
|
it("includes task ID in query parameters", () => {
|
|
@@ -194,7 +194,7 @@ describe("generateEndpointDocs", () => {
|
|
|
194
194
|
|
|
195
195
|
it("generates GET endpoints for all provided resources", () => {
|
|
196
196
|
for (const resource of mockResources) {
|
|
197
|
-
assert.match(docs, new RegExp(`GET http://localhost:
|
|
197
|
+
assert.match(docs, new RegExp(`GET http://localhost:7256${resource.restPath}`), `Missing endpoint for ${resource.uri}`);
|
|
198
198
|
}
|
|
199
199
|
});
|
|
200
200
|
|
|
@@ -203,7 +203,7 @@ describe("generateEndpointDocs", () => {
|
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
it("generates no resource endpoints when resources array is empty", () => {
|
|
206
|
-
const docsNoResources = generateEndpointDocs(
|
|
206
|
+
const docsNoResources = generateEndpointDocs(7256, "test-id", mockTools, []);
|
|
207
207
|
assert.doesNotMatch(docsNoResources, /GET http/);
|
|
208
208
|
});
|
|
209
209
|
});
|