palmier 0.7.6 → 0.7.8
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/agent.d.ts +3 -0
- package/dist/agents/agent.js +1 -1
- package/dist/agents/aider.d.ts +1 -0
- package/dist/agents/aider.js +1 -0
- package/dist/agents/claude.d.ts +1 -0
- package/dist/agents/claude.js +1 -0
- package/dist/agents/cline.d.ts +1 -0
- package/dist/agents/cline.js +1 -0
- package/dist/agents/codex.d.ts +1 -0
- package/dist/agents/codex.js +1 -0
- package/dist/agents/copilot.d.ts +1 -0
- package/dist/agents/copilot.js +1 -0
- package/dist/agents/cursor.d.ts +1 -0
- package/dist/agents/cursor.js +1 -0
- package/dist/agents/deepagents.d.ts +1 -0
- package/dist/agents/deepagents.js +1 -0
- package/dist/agents/droid.d.ts +1 -0
- package/dist/agents/droid.js +1 -0
- package/dist/agents/gemini.d.ts +1 -0
- package/dist/agents/gemini.js +1 -0
- package/dist/agents/goose.d.ts +1 -0
- package/dist/agents/goose.js +1 -0
- package/dist/agents/hermes.d.ts +1 -0
- package/dist/agents/hermes.js +1 -0
- package/dist/agents/kimi.d.ts +1 -0
- package/dist/agents/kimi.js +1 -0
- package/dist/agents/kiro.d.ts +1 -0
- package/dist/agents/kiro.js +1 -0
- package/dist/agents/openclaw.d.ts +1 -0
- package/dist/agents/openclaw.js +2 -2
- package/dist/agents/opencode.d.ts +1 -0
- package/dist/agents/opencode.js +1 -0
- package/dist/agents/qoder.d.ts +1 -0
- package/dist/agents/qoder.js +1 -0
- package/dist/agents/qwen.d.ts +1 -0
- package/dist/agents/qwen.js +1 -0
- package/dist/agents/shared-prompt.js +1 -1
- package/dist/commands/init.js +3 -2
- package/dist/commands/pair.js +3 -3
- 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 +79 -7
- package/dist/nats-client.d.ts +1 -1
- package/dist/nats-client.js +6 -3
- package/dist/pending-requests.d.ts +30 -8
- package/dist/pending-requests.js +28 -15
- package/dist/pwa/assets/index-8cTctVnD.js +120 -0
- package/dist/pwa/assets/index-CSUkBBsQ.css +1 -0
- package/dist/pwa/assets/{web-DnuoxUd4.js → web-BNr628AV.js} +1 -1
- package/dist/pwa/assets/{web-7raT3zOZ.js → web-DyQPewAi.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +12 -16
- package/dist/transports/http-transport.js +6 -3
- package/dist/types.d.ts +4 -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/App.css +66 -0
- package/palmier-server/pwa/src/App.tsx +1 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +65 -2
- package/palmier-server/pwa/src/components/RunsView.tsx +48 -22
- package/palmier-server/pwa/src/components/SessionComposer.tsx +137 -0
- package/palmier-server/pwa/src/components/TabBar.tsx +17 -10
- package/palmier-server/pwa/src/components/TaskForm.tsx +11 -66
- package/palmier-server/pwa/src/components/TaskListView.tsx +17 -283
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +9 -5
- package/palmier-server/pwa/src/draftGuard.ts +24 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +335 -12
- package/palmier-server/pwa/src/pages/PairHost.tsx +6 -3
- package/palmier-server/pwa/src/types.ts +1 -6
- 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 +28 -14
- package/src/agents/agent.ts +5 -1
- package/src/agents/aider.ts +1 -0
- package/src/agents/claude.ts +1 -0
- package/src/agents/cline.ts +1 -0
- package/src/agents/codex.ts +1 -0
- package/src/agents/copilot.ts +1 -0
- package/src/agents/cursor.ts +1 -0
- package/src/agents/deepagents.ts +1 -0
- package/src/agents/droid.ts +1 -0
- package/src/agents/gemini.ts +1 -0
- package/src/agents/goose.ts +1 -0
- package/src/agents/hermes.ts +1 -0
- package/src/agents/kimi.ts +1 -0
- package/src/agents/kiro.ts +1 -0
- package/src/agents/openclaw.ts +2 -2
- package/src/agents/opencode.ts +1 -0
- package/src/agents/qoder.ts +1 -0
- package/src/agents/qwen.ts +1 -0
- package/src/agents/shared-prompt.ts +1 -1
- package/src/commands/init.ts +7 -5
- package/src/commands/pair.ts +3 -3
- 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 +83 -7
- package/src/nats-client.ts +10 -3
- package/src/pending-requests.ts +47 -15
- package/src/rpc-handler.ts +13 -16
- package/src/transports/http-transport.ts +6 -3
- package/src/types.ts +4 -3
- package/test/agent-instructions.test.ts +10 -10
- package/test/pairing.test.ts +2 -2
- package/dist/pwa/assets/index-B-ByUHPS.css +0 -1
- package/dist/pwa/assets/index-uSwkmHBs.js +0 -118
package/src/agents/goose.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class GooseAgent implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
+
supportsYolo = true;
|
|
9
10
|
getPromptCommandLine(prompt: string): CommandLine {
|
|
10
11
|
return { command: "goose", args: ["run", "--text", prompt] };
|
|
11
12
|
}
|
package/src/agents/hermes.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class Hermes implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
+
supportsYolo = true;
|
|
9
10
|
getPromptCommandLine(prompt: string): CommandLine {
|
|
10
11
|
return { command: "hermes", args: ["chat", "-q", prompt] };
|
|
11
12
|
}
|
package/src/agents/kimi.ts
CHANGED
package/src/agents/kiro.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class Kiro implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
+
supportsYolo = true;
|
|
9
10
|
getPromptCommandLine(prompt: string): CommandLine {
|
|
10
11
|
return { command: "kiro-cli", args: ["--no-interactive", prompt] };
|
|
11
12
|
}
|
package/src/agents/openclaw.ts
CHANGED
|
@@ -5,13 +5,13 @@ import { getAgentInstructions } from "./shared-prompt.js";
|
|
|
5
5
|
|
|
6
6
|
export class OpenClawAgent implements AgentTool {
|
|
7
7
|
supportsPermissions = false;
|
|
8
|
+
supportsYolo = false;
|
|
8
9
|
getPromptCommandLine(prompt: string): CommandLine {
|
|
9
10
|
return { command: "openclaw", args: ["agent", "--local", "--agent", "main", "--message", prompt] };
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
13
|
-
const
|
|
14
|
-
const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
|
|
14
|
+
const prompt = followupPrompt ?? getAgentInstructions(task, true);
|
|
15
15
|
// OpenClaw does not support stdin as prompt.
|
|
16
16
|
const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
|
|
17
17
|
|
package/src/agents/opencode.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
|
|
|
6
6
|
|
|
7
7
|
export class OpenCodeAgent implements AgentTool {
|
|
8
8
|
supportsPermissions = false;
|
|
9
|
+
supportsYolo = true;
|
|
9
10
|
getPromptCommandLine(prompt: string): CommandLine {
|
|
10
11
|
return { command: "opencode", args: ["run", prompt] };
|
|
11
12
|
}
|
package/src/agents/qoder.ts
CHANGED
package/src/agents/qwen.ts
CHANGED
|
@@ -16,7 +16,7 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
|
|
|
16
16
|
* Build the full agent prompt: instructions + endpoint docs + task description.
|
|
17
17
|
*/
|
|
18
18
|
export function getAgentInstructions(task: ParsedTask, skipPermissions?: boolean): string {
|
|
19
|
-
const port = loadConfig().httpPort ??
|
|
19
|
+
const port = loadConfig().httpPort ?? 7256;
|
|
20
20
|
const taskDescription = task.frontmatter.user_prompt;
|
|
21
21
|
let instructions = AGENT_INSTRUCTIONS_TEMPLATE
|
|
22
22
|
.replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(port, task.frontmatter.id))
|
package/src/commands/init.ts
CHANGED
|
@@ -45,7 +45,7 @@ export async function initCommand(): Promise<void> {
|
|
|
45
45
|
const lanAnswer = await ask("Enable LAN access (direct HTTP from local network)? (y/N): ");
|
|
46
46
|
const lanEnabled = lanAnswer.trim().toLowerCase() === "y";
|
|
47
47
|
|
|
48
|
-
let httpPort =
|
|
48
|
+
let httpPort = 7256;
|
|
49
49
|
const portLabel = lanEnabled ? "HTTP port for local and LAN access" : "HTTP port for local access";
|
|
50
50
|
const portAnswer = await ask(`${portLabel} (default ${httpPort}): `);
|
|
51
51
|
const parsed = parseInt(portAnswer.trim(), 10);
|
|
@@ -86,7 +86,7 @@ export async function initCommand(): Promise<void> {
|
|
|
86
86
|
try { existingHostId = loadConfig().hostId; } catch { /* first init */ }
|
|
87
87
|
|
|
88
88
|
const serverUrl = "https://app.palmier.me";
|
|
89
|
-
let registerResponse: { hostId: string; natsUrl: string; natsWsUrl: string;
|
|
89
|
+
let registerResponse: { hostId: string; natsUrl: string; natsWsUrl: string; natsJwt: string; natsNkeySeed: string };
|
|
90
90
|
|
|
91
91
|
while (true) {
|
|
92
92
|
console.log(`\nRegistering host...`);
|
|
@@ -111,7 +111,8 @@ export async function initCommand(): Promise<void> {
|
|
|
111
111
|
projectRoot: process.cwd(),
|
|
112
112
|
natsUrl: registerResponse.natsUrl,
|
|
113
113
|
natsWsUrl: registerResponse.natsWsUrl,
|
|
114
|
-
|
|
114
|
+
natsJwt: registerResponse.natsJwt,
|
|
115
|
+
natsNkeySeed: registerResponse.natsNkeySeed,
|
|
115
116
|
agents,
|
|
116
117
|
httpPort,
|
|
117
118
|
lanEnabled,
|
|
@@ -138,7 +139,7 @@ export async function initCommand(): Promise<void> {
|
|
|
138
139
|
async function registerHost(
|
|
139
140
|
serverUrl: string,
|
|
140
141
|
existingHostId?: string,
|
|
141
|
-
): Promise<{ hostId: string; natsUrl: string; natsWsUrl: string;
|
|
142
|
+
): Promise<{ hostId: string; natsUrl: string; natsWsUrl: string; natsJwt: string; natsNkeySeed: string }> {
|
|
142
143
|
try {
|
|
143
144
|
const res = await fetch(`${serverUrl}/api/hosts/register`, {
|
|
144
145
|
method: "POST",
|
|
@@ -155,7 +156,8 @@ async function registerHost(
|
|
|
155
156
|
hostId: string;
|
|
156
157
|
natsUrl: string;
|
|
157
158
|
natsWsUrl: string;
|
|
158
|
-
|
|
159
|
+
natsJwt: string;
|
|
160
|
+
natsNkeySeed: string;
|
|
159
161
|
};
|
|
160
162
|
} catch (err) {
|
|
161
163
|
const message = err instanceof Error ? err.message : String(err);
|
package/src/commands/pair.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { HostConfig } from "../types.js";
|
|
|
8
8
|
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
|
|
9
9
|
const CODE_LENGTH = 6;
|
|
10
10
|
|
|
11
|
-
export const PAIRING_EXPIRY_MS =
|
|
11
|
+
export const PAIRING_EXPIRY_MS = 60 * 1000; // 1 minute
|
|
12
12
|
|
|
13
13
|
export function generatePairingCode(): string {
|
|
14
14
|
const bytes = new Uint8Array(CODE_LENGTH);
|
|
@@ -67,7 +67,7 @@ function httpPairRegister(port: number, code: string): Promise<boolean> {
|
|
|
67
67
|
export async function pairCommand(): Promise<void> {
|
|
68
68
|
const config = loadConfig();
|
|
69
69
|
const code = generatePairingCode();
|
|
70
|
-
const httpPort = config.httpPort ??
|
|
70
|
+
const httpPort = config.httpPort ?? 7256;
|
|
71
71
|
|
|
72
72
|
let paired = false;
|
|
73
73
|
|
|
@@ -84,7 +84,7 @@ export async function pairCommand(): Promise<void> {
|
|
|
84
84
|
console.log("");
|
|
85
85
|
console.log(` ${code}`);
|
|
86
86
|
console.log("");
|
|
87
|
-
console.log("Code expires in
|
|
87
|
+
console.log("Code expires in 1 minute.");
|
|
88
88
|
|
|
89
89
|
// NATS pairing (server mode)
|
|
90
90
|
const nc = await connectNats(config);
|
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
|
@@ -65,7 +65,7 @@ const requestInputTool: ToolDefinition = {
|
|
|
65
65
|
"Request input from the user.",
|
|
66
66
|
"The request blocks until the user responds.",
|
|
67
67
|
'Response: `{"values": ["answer1", "answer2"]}` on success, or `{"aborted": true}` if the user declines.',
|
|
68
|
-
"When you need information from the user (credentials, answers to questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout, even in a non-interactive environment — use this
|
|
68
|
+
"When you need information from the user (credentials, answers to questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout, even in a non-interactive environment — use this instead.",
|
|
69
69
|
],
|
|
70
70
|
inputSchema: {
|
|
71
71
|
type: "object",
|
|
@@ -84,13 +84,18 @@ const requestInputTool: ToolDefinition = {
|
|
|
84
84
|
const { description, questions } = args as { description?: string; questions: string[] };
|
|
85
85
|
if (!questions?.length) throw new ToolError("questions is required", 400);
|
|
86
86
|
|
|
87
|
-
const pendingPromise = registerPending(ctx.sessionId, "input", questions
|
|
87
|
+
const pendingPromise = registerPending(ctx.sessionId, "input", questions, {
|
|
88
|
+
session_id: ctx.sessionId,
|
|
89
|
+
session_name: ctx.agentName,
|
|
90
|
+
description,
|
|
91
|
+
input_questions: questions,
|
|
92
|
+
});
|
|
88
93
|
|
|
89
94
|
await ctx.publishEvent("_input", {
|
|
90
95
|
event_type: "input-request",
|
|
91
96
|
host_id: ctx.config.hostId,
|
|
92
97
|
session_id: ctx.sessionId,
|
|
93
|
-
|
|
98
|
+
session_name: ctx.agentName,
|
|
94
99
|
description,
|
|
95
100
|
input_questions: questions,
|
|
96
101
|
});
|
|
@@ -131,13 +136,17 @@ const requestConfirmationTool: ToolDefinition = {
|
|
|
131
136
|
const { description } = args as { description: string };
|
|
132
137
|
if (!description) throw new ToolError("description is required", 400);
|
|
133
138
|
|
|
134
|
-
const pendingPromise = registerPending(ctx.sessionId, "confirmation"
|
|
139
|
+
const pendingPromise = registerPending(ctx.sessionId, "confirmation", undefined, {
|
|
140
|
+
session_id: ctx.sessionId,
|
|
141
|
+
session_name: ctx.agentName,
|
|
142
|
+
description,
|
|
143
|
+
});
|
|
135
144
|
|
|
136
145
|
await ctx.publishEvent("_confirm", {
|
|
137
146
|
event_type: "confirm-request",
|
|
138
147
|
host_id: ctx.config.hostId,
|
|
139
148
|
session_id: ctx.sessionId,
|
|
140
|
-
|
|
149
|
+
session_name: ctx.agentName,
|
|
141
150
|
description,
|
|
142
151
|
});
|
|
143
152
|
|
|
@@ -159,7 +168,7 @@ const deviceGeolocationTool: ToolDefinition = {
|
|
|
159
168
|
name: "device-geolocation",
|
|
160
169
|
description: [
|
|
161
170
|
"Get the GPS location of the user's mobile device.",
|
|
162
|
-
"When you need the user's real-time location, use this
|
|
171
|
+
"When you need the user's real-time location, use this.",
|
|
163
172
|
"Blocks until the device responds (up to 30 seconds).",
|
|
164
173
|
'Response: `{"latitude": ..., "longitude": ..., "accuracy": ..., "timestamp": ...}` on success, or `{"error": "..."}` on failure.',
|
|
165
174
|
],
|
|
@@ -657,7 +666,74 @@ const setRingerModeTool: ToolDefinition = {
|
|
|
657
666
|
},
|
|
658
667
|
};
|
|
659
668
|
|
|
660
|
-
|
|
669
|
+
const sendEmailTool: ToolDefinition = {
|
|
670
|
+
name: "send-email",
|
|
671
|
+
description: [
|
|
672
|
+
"Send an email from the user's mobile device.",
|
|
673
|
+
"When you need to send an email, use this. The email app opens on the device with the draft pre-filled for the user to review and send.",
|
|
674
|
+
'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
|
|
675
|
+
],
|
|
676
|
+
inputSchema: {
|
|
677
|
+
type: "object",
|
|
678
|
+
properties: {
|
|
679
|
+
to: { type: "string", description: "Recipient email address" },
|
|
680
|
+
subject: { type: "string", description: "Email subject" },
|
|
681
|
+
body: { type: "string", description: "Email body text" },
|
|
682
|
+
cc: { type: "string", description: "CC recipient(s)" },
|
|
683
|
+
bcc: { type: "string", description: "BCC recipient(s)" },
|
|
684
|
+
},
|
|
685
|
+
required: ["to"],
|
|
686
|
+
},
|
|
687
|
+
async handler(args, ctx) {
|
|
688
|
+
if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
689
|
+
|
|
690
|
+
const device = getCapabilityDevice("email");
|
|
691
|
+
if (!device) throw new ToolError("No device has email access enabled", 400);
|
|
692
|
+
|
|
693
|
+
const { to, subject, body, cc, bcc } = args as { to: string; subject?: string; body?: string; cc?: string; bcc?: string };
|
|
694
|
+
if (!to) throw new ToolError("to is required", 400);
|
|
695
|
+
|
|
696
|
+
const sc = StringCodec();
|
|
697
|
+
|
|
698
|
+
const payload: Record<string, string> = {
|
|
699
|
+
hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: device.fcmToken,
|
|
700
|
+
to,
|
|
701
|
+
};
|
|
702
|
+
if (subject) payload.subject = subject;
|
|
703
|
+
if (body) payload.body = body;
|
|
704
|
+
if (cc) payload.cc = cc;
|
|
705
|
+
if (bcc) payload.bcc = bcc;
|
|
706
|
+
|
|
707
|
+
const ackReply = await ctx.nc.request(
|
|
708
|
+
`host.${ctx.config.hostId}.fcm.email`,
|
|
709
|
+
sc.encode(JSON.stringify(payload)),
|
|
710
|
+
{ timeout: 5_000 },
|
|
711
|
+
);
|
|
712
|
+
const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
|
|
713
|
+
if (ack.error) throw new ToolError(ack.error, 502);
|
|
714
|
+
|
|
715
|
+
const responsePromise = new Promise<string>((resolve, reject) => {
|
|
716
|
+
const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.email.${ctx.sessionId}`, { max: 1 });
|
|
717
|
+
const timer = setTimeout(() => {
|
|
718
|
+
sub.unsubscribe();
|
|
719
|
+
reject(new ToolError("Device did not respond within 30 seconds", 504));
|
|
720
|
+
}, 30_000);
|
|
721
|
+
|
|
722
|
+
(async () => {
|
|
723
|
+
for await (const msg of sub) {
|
|
724
|
+
clearTimeout(timer);
|
|
725
|
+
resolve(sc.decode(msg.data));
|
|
726
|
+
}
|
|
727
|
+
})();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const result = JSON.parse(await responsePromise);
|
|
731
|
+
if (result.error) return { error: result.error };
|
|
732
|
+
return result;
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool, readContactsTool, createContactTool, readCalendarTool, createCalendarEventTool, sendSmsTool, sendEmailTool, sendAlertTool, readBatteryTool, setRingerModeTool];
|
|
661
737
|
export const agentToolMap = new Map<string, ToolDefinition>(agentTools.map((t) => [t.name, t]));
|
|
662
738
|
|
|
663
739
|
// ── 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/pending-requests.ts
CHANGED
|
@@ -1,30 +1,44 @@
|
|
|
1
1
|
import type { RequiredPermission } from "./types.js";
|
|
2
2
|
|
|
3
|
+
export interface PendingRequestMeta {
|
|
4
|
+
/** Doubles as task_id for permission-type entries (the key the task uses). */
|
|
5
|
+
session_id?: string;
|
|
6
|
+
/** Human-readable label for whoever opened the prompt — agent name for
|
|
7
|
+
* confirm/input, task name for permission. */
|
|
8
|
+
session_name?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
input_questions?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
export interface PendingRequest {
|
|
4
14
|
type: "confirmation" | "permission" | "input";
|
|
5
15
|
resolve: (value: string[]) => void;
|
|
6
16
|
/** Permission list (for 'permission') or input descriptions (for 'input'). */
|
|
7
17
|
params?: RequiredPermission[] | string[];
|
|
18
|
+
/** Display context for PWAs that connect while this request is already open. */
|
|
19
|
+
meta?: PendingRequestMeta;
|
|
8
20
|
}
|
|
9
21
|
|
|
10
22
|
const pending = new Map<string, PendingRequest>();
|
|
11
23
|
|
|
12
24
|
/**
|
|
13
|
-
* Register a pending request
|
|
14
|
-
*
|
|
15
|
-
*
|
|
25
|
+
* Register a pending request keyed by either a sessionId (confirmation / input)
|
|
26
|
+
* or a taskId (permission). The `meta` is surfaced to PWAs that connect after
|
|
27
|
+
* the request was opened, so their modals can render without replaying events.
|
|
28
|
+
* Only one pending request per key at a time.
|
|
16
29
|
*/
|
|
17
30
|
export function registerPending(
|
|
18
|
-
|
|
31
|
+
key: string,
|
|
19
32
|
type: PendingRequest["type"],
|
|
20
33
|
params?: PendingRequest["params"],
|
|
34
|
+
meta?: PendingRequestMeta,
|
|
21
35
|
): Promise<string[]> {
|
|
22
|
-
if (pending.has(
|
|
23
|
-
return Promise.reject(new Error(`
|
|
36
|
+
if (pending.has(key)) {
|
|
37
|
+
return Promise.reject(new Error(`Key ${key} already has a pending request`));
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
return new Promise<string[]>((resolve) => {
|
|
27
|
-
pending.set(
|
|
41
|
+
pending.set(key, { type, resolve, params, meta });
|
|
28
42
|
});
|
|
29
43
|
}
|
|
30
44
|
|
|
@@ -32,24 +46,42 @@ export function registerPending(
|
|
|
32
46
|
* Resolve a pending request with the user's response.
|
|
33
47
|
* Returns true if a pending request was found and resolved.
|
|
34
48
|
*/
|
|
35
|
-
export function resolvePending(
|
|
36
|
-
const entry = pending.get(
|
|
49
|
+
export function resolvePending(key: string, value: string[]): boolean {
|
|
50
|
+
const entry = pending.get(key);
|
|
37
51
|
if (!entry) return false;
|
|
38
|
-
pending.delete(
|
|
52
|
+
pending.delete(key);
|
|
39
53
|
entry.resolve(value);
|
|
40
54
|
return true;
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
/**
|
|
44
|
-
* Get the current pending request for a
|
|
58
|
+
* Get the current pending request for a key (if any).
|
|
45
59
|
*/
|
|
46
|
-
export function getPending(
|
|
47
|
-
return pending.get(
|
|
60
|
+
export function getPending(key: string): PendingRequest | undefined {
|
|
61
|
+
return pending.get(key);
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
/**
|
|
51
65
|
* Remove a pending request without resolving it.
|
|
52
66
|
*/
|
|
53
|
-
export function removePending(
|
|
54
|
-
pending.delete(
|
|
67
|
+
export function removePending(key: string): void {
|
|
68
|
+
pending.delete(key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* List all currently-pending requests, stripped of the unserializable `resolve`
|
|
73
|
+
* callback. Used by `host.info` so the PWA can seed its modal state on connect.
|
|
74
|
+
*/
|
|
75
|
+
export function listPending(): Array<{
|
|
76
|
+
key: string;
|
|
77
|
+
type: PendingRequest["type"];
|
|
78
|
+
params?: PendingRequest["params"];
|
|
79
|
+
meta?: PendingRequestMeta;
|
|
80
|
+
}> {
|
|
81
|
+
return [...pending.entries()].map(([key, entry]) => ({
|
|
82
|
+
key,
|
|
83
|
+
type: entry.type,
|
|
84
|
+
params: entry.params,
|
|
85
|
+
meta: entry.meta,
|
|
86
|
+
}));
|
|
55
87
|
}
|
package/src/rpc-handler.ts
CHANGED
|
@@ -4,7 +4,7 @@ import * as path from "path";
|
|
|
4
4
|
import { spawn, type ChildProcess } from "child_process";
|
|
5
5
|
import { type NatsConnection } from "nats";
|
|
6
6
|
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
|
|
7
|
-
import { resolvePending, getPending } from "./pending-requests.js";
|
|
7
|
+
import { resolvePending, getPending, listPending } from "./pending-requests.js";
|
|
8
8
|
import { getPlatform } from "./platform/index.js";
|
|
9
9
|
import { spawnCommand } from "./spawn-command.js";
|
|
10
10
|
import crossSpawn from "cross-spawn";
|
|
@@ -142,13 +142,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
142
142
|
function flattenTask(task: ParsedTask) {
|
|
143
143
|
const taskDir = getTaskDir(config.projectRoot, task.frontmatter.id);
|
|
144
144
|
const status = readTaskStatus(taskDir);
|
|
145
|
-
const pending = getPending(task.frontmatter.id);
|
|
146
145
|
return {
|
|
147
146
|
...task.frontmatter,
|
|
148
|
-
status: status
|
|
149
|
-
...status,
|
|
150
|
-
...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
|
|
151
|
-
} : undefined,
|
|
147
|
+
status: status ?? undefined,
|
|
152
148
|
};
|
|
153
149
|
}
|
|
154
150
|
|
|
@@ -161,22 +157,28 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
161
157
|
}
|
|
162
158
|
|
|
163
159
|
switch (request.method) {
|
|
164
|
-
case "
|
|
165
|
-
|
|
160
|
+
case "host.info": {
|
|
161
|
+
// Bootstrap metadata the PWA needs on connect, independent of which tab
|
|
162
|
+
// is active. Includes any prompts already waiting so a reconnecting
|
|
163
|
+
// PWA can render their modals without replaying events.
|
|
166
164
|
const capabilities: Record<string, string | null> = {};
|
|
167
165
|
for (const cap of ["location", "notifications", "sms", "contacts", "calendar", "alert", "battery", "dnd"] as const) {
|
|
168
166
|
capabilities[cap] = getCapabilityDevice(cap)?.clientToken ?? null;
|
|
169
167
|
}
|
|
170
168
|
return {
|
|
171
|
-
tasks: tasks.map((task) => flattenTask(task)),
|
|
172
169
|
agents: config.agents ?? [],
|
|
173
170
|
version: currentVersion,
|
|
174
171
|
host_platform: process.platform,
|
|
175
|
-
location_client_token: capabilities.location,
|
|
176
172
|
capability_tokens: capabilities,
|
|
173
|
+
pending_prompts: listPending(),
|
|
177
174
|
};
|
|
178
175
|
}
|
|
179
176
|
|
|
177
|
+
case "task.list": {
|
|
178
|
+
const tasks = listTasks(config.projectRoot);
|
|
179
|
+
return { tasks: tasks.map((task) => flattenTask(task)) };
|
|
180
|
+
}
|
|
181
|
+
|
|
180
182
|
case "task.get": {
|
|
181
183
|
const params = request.params as { id: string };
|
|
182
184
|
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
@@ -537,12 +539,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
537
539
|
if (!status) {
|
|
538
540
|
return { task_id: params.id, error: "No status found" };
|
|
539
541
|
}
|
|
540
|
-
|
|
541
|
-
return {
|
|
542
|
-
task_id: params.id,
|
|
543
|
-
...status,
|
|
544
|
-
...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
|
|
545
|
-
};
|
|
542
|
+
return { task_id: params.id, ...status };
|
|
546
543
|
}
|
|
547
544
|
|
|
548
545
|
case "task.result": {
|
|
@@ -306,7 +306,7 @@ export async function startHttpTransport(
|
|
|
306
306
|
const timer = setTimeout(() => {
|
|
307
307
|
pendingPairs.delete(code);
|
|
308
308
|
resolve({ paired: false });
|
|
309
|
-
}, expiryMs ??
|
|
309
|
+
}, expiryMs ?? 60 * 1000);
|
|
310
310
|
|
|
311
311
|
pendingPairs.set(code, { resolve, timer });
|
|
312
312
|
req.on("close", () => {
|
|
@@ -336,13 +336,16 @@ export async function startHttpTransport(
|
|
|
336
336
|
return;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
const pendingPromise = registerPending(taskId, "permission", permissions
|
|
339
|
+
const pendingPromise = registerPending(taskId, "permission", permissions, {
|
|
340
|
+
session_id: taskId,
|
|
341
|
+
session_name: taskName,
|
|
342
|
+
});
|
|
340
343
|
|
|
341
344
|
await publishEvent(taskId, {
|
|
342
345
|
event_type: "permission-request",
|
|
343
346
|
host_id: config.hostId,
|
|
344
347
|
required_permissions: permissions,
|
|
345
|
-
|
|
348
|
+
session_name: taskName,
|
|
346
349
|
});
|
|
347
350
|
|
|
348
351
|
const response = await pendingPromise;
|
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
|
-
agents?: Array<{ key: string; label: string }>;
|
|
11
|
+
agents?: Array<{ key: string; label: string; supportsPermissions: boolean; supportsYolo: boolean }>;
|
|
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;
|