palmier 0.6.5 → 0.6.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/README.md +1 -1
- package/dist/agents/agent-instructions.md +28 -6
- package/dist/agents/agent.js +6 -3
- package/dist/agents/hermes.d.ts +9 -0
- package/dist/agents/hermes.js +35 -0
- package/dist/commands/plan-generation.md +1 -0
- package/dist/commands/run.js +3 -3
- package/dist/location-device.d.ts +8 -0
- package/dist/location-device.js +32 -0
- package/dist/mcp-handler.d.ts +8 -0
- package/dist/mcp-handler.js +110 -0
- package/dist/mcp-tools.d.ts +22 -0
- package/dist/mcp-tools.js +152 -0
- package/dist/pwa/assets/{index-DhvJN8ie.css → index-DAI3J-jU.css} +1 -1
- package/dist/pwa/assets/index-RrJvjqz9.js +118 -0
- package/dist/pwa/assets/web-DQteXlI7.js +1 -0
- package/dist/pwa/assets/web-EzNEHXEh.js +1 -0
- package/dist/pwa/index.html +3 -3
- package/dist/pwa/service-worker.js +2 -2
- package/dist/rpc-handler.js +23 -15
- package/dist/transports/http-transport.js +61 -129
- package/package.json +1 -1
- package/palmier-server/README.md +6 -1
- package/palmier-server/package.json +7 -1
- package/palmier-server/pnpm-lock.yaml +1025 -1
- package/palmier-server/pwa/index.html +1 -1
- package/palmier-server/pwa/package.json +3 -0
- package/palmier-server/pwa/src/App.css +55 -0
- package/palmier-server/pwa/src/api.ts +8 -2
- package/palmier-server/pwa/src/components/HostMenu.tsx +102 -1
- package/palmier-server/pwa/src/components/TaskListView.tsx +94 -78
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +2 -1
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +3 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +5 -2
- package/palmier-server/pwa/src/pages/PairHost.tsx +10 -1
- package/palmier-server/pwa/src/service-worker.ts +7 -7
- package/palmier-server/server/.env.example +4 -0
- package/palmier-server/server/package.json +1 -0
- package/palmier-server/server/src/db.ts +10 -0
- package/palmier-server/server/src/fcm.ts +74 -0
- package/palmier-server/server/src/index.ts +101 -21
- package/palmier-server/server/src/notify.ts +34 -0
- package/palmier-server/server/src/push.ts +1 -1
- package/palmier-server/server/src/routes/fcm.ts +64 -0
- package/palmier-server/server/src/routes/push.ts +6 -5
- package/palmier-server/spec.md +4 -2
- package/src/agents/agent-instructions.md +28 -6
- package/src/agents/agent.ts +6 -3
- package/src/agents/hermes.ts +38 -0
- package/src/commands/plan-generation.md +1 -0
- package/src/commands/run.ts +3 -3
- package/src/location-device.ts +35 -0
- package/src/mcp-handler.ts +133 -0
- package/src/mcp-tools.ts +182 -0
- package/src/rpc-handler.ts +24 -15
- package/src/transports/http-transport.ts +58 -128
- package/dist/pwa/assets/index-CXqKVvmk.js +0 -118
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.
|
|
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, and fetching GPS location.
|
|
38
38
|
|
|
39
39
|
```
|
|
40
40
|
┌──────────────┐ HTTP ┌──────────────────┐
|
|
@@ -18,18 +18,40 @@ If the task fails because a tool was denied or you lack the required permissions
|
|
|
18
18
|
|
|
19
19
|
## HTTP Endpoints
|
|
20
20
|
|
|
21
|
-
The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution. Use curl to call them.
|
|
21
|
+
The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution. Use curl to call them. All endpoints require `taskId` in the request body.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
**`POST /request-input`** — Request input from the user. The request blocks until the user responds.
|
|
24
24
|
```json
|
|
25
|
-
{"taskId":"{{TASK_ID}}","
|
|
25
|
+
{"taskId": "{{TASK_ID}}", "description": "optional context", "questions": ["question 1", "question 2"]}
|
|
26
26
|
```
|
|
27
|
-
|
|
27
|
+
- `taskId` (required, string): The current task ID.
|
|
28
|
+
- `questions` (required, string array): Questions to present to the user.
|
|
29
|
+
- `description` (optional, string): Context or heading for the input request.
|
|
30
|
+
- Response: `{"values": ["answer1", "answer2"]}` on success, or `{"aborted": true}` if the user declines.
|
|
31
|
+
- 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 endpoint instead.
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
**`POST /request-confirmation`** — Request confirmation from the user. The request blocks until the user confirms or aborts.
|
|
30
34
|
```json
|
|
31
|
-
{"taskId":"{{TASK_ID}}","
|
|
35
|
+
{"taskId": "{{TASK_ID}}", "description": "What the user is confirming"}
|
|
32
36
|
```
|
|
37
|
+
- `taskId` (required, string): The current task ID.
|
|
38
|
+
- `description` (required, string): What the user is confirming.
|
|
39
|
+
- Response: `{"confirmed": true}` or `{"confirmed": false}`.
|
|
40
|
+
|
|
41
|
+
**`POST /device-geolocation`** — Get the GPS location of the user's mobile device. Blocks until the device responds (up to 30 seconds).
|
|
42
|
+
```json
|
|
43
|
+
{"taskId": "{{TASK_ID}}"}
|
|
44
|
+
```
|
|
45
|
+
- `taskId` (required, string): The current task ID.
|
|
46
|
+
- Response: `{"latitude": ..., "longitude": ..., "accuracy": ..., "timestamp": ...}` on success, or `{"error": "..."}` on failure.
|
|
47
|
+
|
|
48
|
+
**`POST /notify`** — Send a push notification to the user's device.
|
|
49
|
+
```json
|
|
50
|
+
{"taskId": "{{TASK_ID}}", "title": "...", "body": "..."}
|
|
51
|
+
```
|
|
52
|
+
- `taskId` (required, string): The current task ID.
|
|
53
|
+
- `title` (required, string): Notification title.
|
|
54
|
+
- `body` (required, string): Notification body.
|
|
33
55
|
|
|
34
56
|
---
|
|
35
57
|
|
package/dist/agents/agent.js
CHANGED
|
@@ -14,6 +14,7 @@ import { Cursor } from "./cursor.js";
|
|
|
14
14
|
import { Kiro } from "./kiro.js";
|
|
15
15
|
import { Cline } from "./cline.js";
|
|
16
16
|
import { Qoder } from "./qoder.js";
|
|
17
|
+
import { Hermes } from "./hermes.js";
|
|
17
18
|
const agentRegistry = {
|
|
18
19
|
claude: new ClaudeAgent(),
|
|
19
20
|
gemini: new GeminiAgent(),
|
|
@@ -31,6 +32,7 @@ const agentRegistry = {
|
|
|
31
32
|
kiro: new Kiro(),
|
|
32
33
|
cline: new Cline(),
|
|
33
34
|
qoder: new Qoder(),
|
|
35
|
+
hermes: new Hermes(),
|
|
34
36
|
};
|
|
35
37
|
const agentLabels = {
|
|
36
38
|
claude: "Claude Code",
|
|
@@ -46,9 +48,10 @@ const agentLabels = {
|
|
|
46
48
|
deepagents: "Deep Agents CLI",
|
|
47
49
|
aider: "Aider",
|
|
48
50
|
cursor: "Cursor CLI",
|
|
49
|
-
kiro: "Kiro",
|
|
50
|
-
cline: "Cline",
|
|
51
|
-
qoder: "Qoder",
|
|
51
|
+
kiro: "Kiro CLI",
|
|
52
|
+
cline: "Cline CLI",
|
|
53
|
+
qoder: "Qoder CLI",
|
|
54
|
+
hermes: "Hermes Agent",
|
|
52
55
|
};
|
|
53
56
|
export async function detectAgents() {
|
|
54
57
|
const detected = [];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
3
|
+
export declare class Hermes implements AgentTool {
|
|
4
|
+
supportsPermissions: boolean;
|
|
5
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine;
|
|
6
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine;
|
|
7
|
+
init(): Promise<boolean>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=hermes.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { getAgentInstructions } from "./shared-prompt.js";
|
|
3
|
+
import { SHELL } from "../platform/index.js";
|
|
4
|
+
export class Hermes {
|
|
5
|
+
supportsPermissions = false;
|
|
6
|
+
getPlanGenerationCommandLine(prompt) {
|
|
7
|
+
return {
|
|
8
|
+
command: "hermes",
|
|
9
|
+
args: ["chat", "-q", prompt],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
|
|
13
|
+
const yolo = extraPermissions === "yolo";
|
|
14
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
15
|
+
const args = ["chat"];
|
|
16
|
+
if (yolo) {
|
|
17
|
+
args.push("--trust-all-tools");
|
|
18
|
+
}
|
|
19
|
+
if (followupPrompt) {
|
|
20
|
+
args.push("--continue");
|
|
21
|
+
} // continue mode for followups
|
|
22
|
+
args.push("-q", prompt);
|
|
23
|
+
return { command: "hermes", args };
|
|
24
|
+
}
|
|
25
|
+
async init() {
|
|
26
|
+
try {
|
|
27
|
+
execSync("hermes --version", { stdio: "ignore", shell: SHELL });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=hermes.js.map
|
|
@@ -16,6 +16,7 @@ task_name: <concise label, 3-6 words>
|
|
|
16
16
|
- If the task produces formatted output (report, email, summary, etc.), specify the structure, sections, and tone.
|
|
17
17
|
- When a step requires user input, simply state what information is needed from the user. Do **not** specify how to obtain it — the agent has its own tool for requesting user input.
|
|
18
18
|
- Preserve relative time expressions (e.g., "today", "yesterday", "last week") exactly as written — do **not** resolve them to specific dates. The plan may be executed on a different day than it was generated.
|
|
19
|
+
- If the task involves opening a web browser or application, include a final step to close it before finishing.
|
|
19
20
|
|
|
20
21
|
## Task Description
|
|
21
22
|
|
package/dist/commands/run.js
CHANGED
|
@@ -39,7 +39,7 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
39
39
|
console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
|
|
40
40
|
const result = await spawnCommand(command, args, {
|
|
41
41
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
42
|
-
env: { ...ctx.guiEnv, ...agentEnv,
|
|
42
|
+
env: { ...ctx.guiEnv, ...agentEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
|
|
43
43
|
echoStdout: true,
|
|
44
44
|
resolveOnFailure: true,
|
|
45
45
|
stdin,
|
|
@@ -261,7 +261,7 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
261
261
|
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
262
262
|
const child = spawnStreamingCommand(commandStr, {
|
|
263
263
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
264
|
-
env: { ...ctx.guiEnv,
|
|
264
|
+
env: { ...ctx.guiEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
|
|
265
265
|
});
|
|
266
266
|
let linesProcessed = 0;
|
|
267
267
|
let invocationsSucceeded = 0;
|
|
@@ -424,7 +424,7 @@ async function requestConfirmation(config, task, taskDir) {
|
|
|
424
424
|
const res = await fetch(`http://localhost:${port}/request-confirmation`, {
|
|
425
425
|
method: "POST",
|
|
426
426
|
headers: { "Content-Type": "application/json" },
|
|
427
|
-
body: JSON.stringify({ taskId: task.frontmatter.id,
|
|
427
|
+
body: JSON.stringify({ taskId: task.frontmatter.id, description: `Run task "${task.frontmatter.name || task.frontmatter.id}"?` }),
|
|
428
428
|
});
|
|
429
429
|
const body = await res.json();
|
|
430
430
|
if (typeof body.confirmed !== "boolean") {
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ToolContext } from "./mcp-tools.js";
|
|
2
|
+
export interface McpResponse {
|
|
3
|
+
body: object;
|
|
4
|
+
sessionId?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function getAgentName(sessionId: string): string | undefined;
|
|
7
|
+
export declare function handleMcpRequest(body: string, sessionId: string | undefined, ctx: ToolContext): Promise<McpResponse>;
|
|
8
|
+
//# sourceMappingURL=mcp-handler.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { agentTools, agentToolMap, ToolError } from "./mcp-tools.js";
|
|
3
|
+
// Session-to-agent name map with 24h TTL
|
|
4
|
+
const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
const sessionAgents = new Map();
|
|
6
|
+
export function getAgentName(sessionId) {
|
|
7
|
+
const entry = sessionAgents.get(sessionId);
|
|
8
|
+
if (!entry)
|
|
9
|
+
return undefined;
|
|
10
|
+
if (Date.now() > entry.expiresAt) {
|
|
11
|
+
sessionAgents.delete(sessionId);
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return entry.agentName;
|
|
15
|
+
}
|
|
16
|
+
function pruneExpiredSessions() {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
for (const [id, entry] of sessionAgents) {
|
|
19
|
+
if (now > entry.expiresAt)
|
|
20
|
+
sessionAgents.delete(id);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function rpcError(id, code, message) {
|
|
24
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
25
|
+
}
|
|
26
|
+
function rpcResult(id, result) {
|
|
27
|
+
return { jsonrpc: "2.0", id, result };
|
|
28
|
+
}
|
|
29
|
+
export async function handleMcpRequest(body, sessionId, ctx) {
|
|
30
|
+
let req;
|
|
31
|
+
try {
|
|
32
|
+
req = JSON.parse(body);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { body: rpcError(null, -32700, "Parse error") };
|
|
36
|
+
}
|
|
37
|
+
const id = req.id ?? null;
|
|
38
|
+
if (req.jsonrpc !== "2.0") {
|
|
39
|
+
return { body: rpcError(id, -32600, "Invalid Request: missing jsonrpc 2.0") };
|
|
40
|
+
}
|
|
41
|
+
const agent = sessionId ? getAgentName(sessionId) : undefined;
|
|
42
|
+
const sid = sessionId?.slice(0, 8) ?? "none";
|
|
43
|
+
const logPrefix = agent ? `[mcp] [${sid}] [${agent}]` : `[mcp] [${sid}]`;
|
|
44
|
+
console.log(`${logPrefix} ${req.method}${req.method === "tools/call" ? ` → ${req.params?.name}` : ""}`);
|
|
45
|
+
switch (req.method) {
|
|
46
|
+
case "initialize": {
|
|
47
|
+
const newSessionId = randomUUID();
|
|
48
|
+
const clientInfo = req.params?.clientInfo;
|
|
49
|
+
const agentName = clientInfo
|
|
50
|
+
? `${clientInfo.name || "unknown"}${clientInfo.version ? ` ${clientInfo.version}` : ""}`
|
|
51
|
+
: undefined;
|
|
52
|
+
if (agentName) {
|
|
53
|
+
sessionAgents.set(newSessionId, { agentName, expiresAt: Date.now() + SESSION_TTL_MS });
|
|
54
|
+
pruneExpiredSessions();
|
|
55
|
+
}
|
|
56
|
+
console.log(`[mcp] [${newSessionId.slice(0, 8)}] Session initialized${agentName ? ` (${agentName})` : ""}`);
|
|
57
|
+
return {
|
|
58
|
+
body: rpcResult(id, {
|
|
59
|
+
protocolVersion: "2025-03-26",
|
|
60
|
+
capabilities: { tools: {} },
|
|
61
|
+
serverInfo: { name: "palmier", version: "1.0.0" },
|
|
62
|
+
}),
|
|
63
|
+
sessionId: newSessionId,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
case "tools/list": {
|
|
67
|
+
return {
|
|
68
|
+
body: rpcResult(id, {
|
|
69
|
+
tools: agentTools.map((t) => ({
|
|
70
|
+
name: t.name,
|
|
71
|
+
description: t.description,
|
|
72
|
+
inputSchema: t.inputSchema,
|
|
73
|
+
})),
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
case "tools/call": {
|
|
78
|
+
const name = req.params?.name;
|
|
79
|
+
const args = (req.params?.arguments ?? {});
|
|
80
|
+
if (!name)
|
|
81
|
+
return { body: rpcError(id, -32602, "Missing params.name") };
|
|
82
|
+
const tool = agentToolMap.get(name);
|
|
83
|
+
if (!tool)
|
|
84
|
+
return { body: rpcError(id, -32602, `Unknown tool: ${name}`) };
|
|
85
|
+
try {
|
|
86
|
+
const result = await tool.handler(args, ctx);
|
|
87
|
+
console.log(`${logPrefix} tools/call ${name} done:`, JSON.stringify(result).slice(0, 200));
|
|
88
|
+
return {
|
|
89
|
+
body: rpcResult(id, {
|
|
90
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
91
|
+
}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const message = err instanceof ToolError ? err.message : String(err);
|
|
96
|
+
console.error(`${logPrefix} tools/call ${name} error:`, message);
|
|
97
|
+
return {
|
|
98
|
+
body: rpcResult(id, {
|
|
99
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
100
|
+
isError: true,
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
default:
|
|
106
|
+
console.warn(`${logPrefix} Unknown method: ${req.method}`);
|
|
107
|
+
return { body: rpcError(id, -32601, `Method not found: ${req.method}`) };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=mcp-handler.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type NatsConnection } from "nats";
|
|
2
|
+
import type { HostConfig } from "./types.js";
|
|
3
|
+
export declare class ToolError extends Error {
|
|
4
|
+
statusCode: number;
|
|
5
|
+
constructor(message: string, statusCode?: number);
|
|
6
|
+
}
|
|
7
|
+
export interface ToolContext {
|
|
8
|
+
config: HostConfig;
|
|
9
|
+
nc: NatsConnection | undefined;
|
|
10
|
+
publishEvent: (id: string, payload: Record<string, unknown>) => Promise<void>;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
agentName?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ToolDefinition {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
inputSchema: object;
|
|
18
|
+
handler: (args: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
|
|
19
|
+
}
|
|
20
|
+
export declare const agentTools: ToolDefinition[];
|
|
21
|
+
export declare const agentToolMap: Map<string, ToolDefinition>;
|
|
22
|
+
//# sourceMappingURL=mcp-tools.d.ts.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { StringCodec } from "nats";
|
|
2
|
+
import { registerPending } from "./pending-requests.js";
|
|
3
|
+
import { getLocationDevice } from "./location-device.js";
|
|
4
|
+
export class ToolError extends Error {
|
|
5
|
+
statusCode;
|
|
6
|
+
constructor(message, statusCode = 500) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.statusCode = statusCode;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const notifyTool = {
|
|
12
|
+
name: "notify",
|
|
13
|
+
description: "Send a push notification to the user's device.",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
title: { type: "string", description: "Notification title" },
|
|
18
|
+
body: { type: "string", description: "Notification body" },
|
|
19
|
+
},
|
|
20
|
+
required: ["title", "body"],
|
|
21
|
+
},
|
|
22
|
+
async handler(args, ctx) {
|
|
23
|
+
const { title, body } = args;
|
|
24
|
+
if (!title || !body)
|
|
25
|
+
throw new ToolError("title and body are required", 400);
|
|
26
|
+
if (!ctx.nc)
|
|
27
|
+
throw new ToolError("NATS not connected — push notifications require server mode", 503);
|
|
28
|
+
const sc = StringCodec();
|
|
29
|
+
const payload = { hostId: ctx.config.hostId, title, body };
|
|
30
|
+
if (ctx.sessionId)
|
|
31
|
+
payload.session_id = ctx.sessionId;
|
|
32
|
+
if (ctx.agentName)
|
|
33
|
+
payload.agent_name = ctx.agentName;
|
|
34
|
+
const subject = `host.${ctx.config.hostId}.push.send`;
|
|
35
|
+
const reply = await ctx.nc.request(subject, sc.encode(JSON.stringify(payload)), { timeout: 15_000 });
|
|
36
|
+
const result = JSON.parse(sc.decode(reply.data));
|
|
37
|
+
if (result.ok)
|
|
38
|
+
return { ok: true };
|
|
39
|
+
throw new ToolError(result.error ?? "Push notification failed", 502);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const requestInputTool = {
|
|
43
|
+
name: "request-input",
|
|
44
|
+
description: "Request input from the user. The request blocks until the user responds.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
description: { type: "string", description: "Context or heading for the input request" },
|
|
49
|
+
questions: {
|
|
50
|
+
type: "array",
|
|
51
|
+
items: { type: "string" },
|
|
52
|
+
description: "Questions to present to the user",
|
|
53
|
+
minItems: 1,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
required: ["questions"],
|
|
57
|
+
},
|
|
58
|
+
async handler(args, ctx) {
|
|
59
|
+
const { description, questions } = args;
|
|
60
|
+
if (!questions?.length)
|
|
61
|
+
throw new ToolError("questions is required", 400);
|
|
62
|
+
const pendingPromise = registerPending(ctx.sessionId, "input", questions);
|
|
63
|
+
await ctx.publishEvent("_input", {
|
|
64
|
+
event_type: "input-request",
|
|
65
|
+
host_id: ctx.config.hostId,
|
|
66
|
+
session_id: ctx.sessionId,
|
|
67
|
+
agent_name: ctx.agentName,
|
|
68
|
+
description,
|
|
69
|
+
input_questions: questions,
|
|
70
|
+
});
|
|
71
|
+
const response = await pendingPromise;
|
|
72
|
+
if (response.length === 1 && response[0] === "aborted") {
|
|
73
|
+
await ctx.publishEvent("_input", { event_type: "input-resolved", host_id: ctx.config.hostId, session_id: ctx.sessionId, status: "aborted" });
|
|
74
|
+
return { aborted: true };
|
|
75
|
+
}
|
|
76
|
+
await ctx.publishEvent("_input", { event_type: "input-resolved", host_id: ctx.config.hostId, session_id: ctx.sessionId, status: "provided" });
|
|
77
|
+
return { values: response };
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const requestConfirmationTool = {
|
|
81
|
+
name: "request-confirmation",
|
|
82
|
+
description: "Request confirmation from the user. The request blocks until the user confirms or aborts.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
description: { type: "string", description: "What the user is confirming" },
|
|
87
|
+
},
|
|
88
|
+
required: ["description"],
|
|
89
|
+
},
|
|
90
|
+
async handler(args, ctx) {
|
|
91
|
+
const { description } = args;
|
|
92
|
+
if (!description)
|
|
93
|
+
throw new ToolError("description is required", 400);
|
|
94
|
+
const pendingPromise = registerPending(ctx.sessionId, "confirmation");
|
|
95
|
+
await ctx.publishEvent("_confirm", {
|
|
96
|
+
event_type: "confirm-request",
|
|
97
|
+
host_id: ctx.config.hostId,
|
|
98
|
+
session_id: ctx.sessionId,
|
|
99
|
+
agent_name: ctx.agentName,
|
|
100
|
+
description,
|
|
101
|
+
});
|
|
102
|
+
const response = await pendingPromise;
|
|
103
|
+
const confirmed = response[0] === "confirmed";
|
|
104
|
+
await ctx.publishEvent("_confirm", {
|
|
105
|
+
event_type: "confirm-resolved",
|
|
106
|
+
host_id: ctx.config.hostId,
|
|
107
|
+
session_id: ctx.sessionId,
|
|
108
|
+
status: confirmed ? "confirmed" : "aborted",
|
|
109
|
+
});
|
|
110
|
+
return { confirmed };
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
const deviceGeolocationTool = {
|
|
114
|
+
name: "device-geolocation",
|
|
115
|
+
description: "Get the GPS location of the user's mobile device. Blocks until the device responds (up to 30 seconds).",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {},
|
|
119
|
+
},
|
|
120
|
+
async handler(_args, ctx) {
|
|
121
|
+
if (!ctx.nc)
|
|
122
|
+
throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
123
|
+
const locDevice = getLocationDevice();
|
|
124
|
+
if (!locDevice)
|
|
125
|
+
throw new ToolError("No device has location access enabled", 400);
|
|
126
|
+
const sc = StringCodec();
|
|
127
|
+
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.geolocation`, sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: locDevice.fcmToken })), { timeout: 5_000 });
|
|
128
|
+
const ack = JSON.parse(sc.decode(ackReply.data));
|
|
129
|
+
if (ack.error)
|
|
130
|
+
throw new ToolError(ack.error, 502);
|
|
131
|
+
const locationPromise = new Promise((resolve, reject) => {
|
|
132
|
+
const sub = ctx.nc.subscribe(`host.${ctx.config.hostId}.geolocation.${ctx.sessionId}`, { max: 1 });
|
|
133
|
+
const timer = setTimeout(() => {
|
|
134
|
+
sub.unsubscribe();
|
|
135
|
+
reject(new ToolError("Device did not respond within 30 seconds", 504));
|
|
136
|
+
}, 30_000);
|
|
137
|
+
(async () => {
|
|
138
|
+
for await (const msg of sub) {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
resolve(sc.decode(msg.data));
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
});
|
|
144
|
+
const locationData = JSON.parse(await locationPromise);
|
|
145
|
+
if (locationData.error)
|
|
146
|
+
return { error: locationData.error };
|
|
147
|
+
return locationData;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
export const agentTools = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool];
|
|
151
|
+
export const agentToolMap = new Map(agentTools.map((t) => [t.name, t]));
|
|
152
|
+
//# sourceMappingURL=mcp-tools.js.map
|