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/src/commands/run.ts
CHANGED
|
@@ -70,7 +70,7 @@ async function invokeAgentWithRetries(
|
|
|
70
70
|
console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
|
|
71
71
|
const result = await spawnCommand(command, args, {
|
|
72
72
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
73
|
-
env: { ...ctx.guiEnv, ...agentEnv,
|
|
73
|
+
env: { ...ctx.guiEnv, ...agentEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
|
|
74
74
|
echoStdout: true,
|
|
75
75
|
resolveOnFailure: true,
|
|
76
76
|
stdin,
|
|
@@ -324,7 +324,7 @@ async function runCommandTriggeredMode(
|
|
|
324
324
|
|
|
325
325
|
const child = spawnStreamingCommand(commandStr, {
|
|
326
326
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
327
|
-
env: { ...ctx.guiEnv,
|
|
327
|
+
env: { ...ctx.guiEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
|
|
328
328
|
});
|
|
329
329
|
|
|
330
330
|
let linesProcessed = 0;
|
|
@@ -519,7 +519,7 @@ async function requestConfirmation(
|
|
|
519
519
|
const res = await fetch(`http://localhost:${port}/request-confirmation`, {
|
|
520
520
|
method: "POST",
|
|
521
521
|
headers: { "Content-Type": "application/json" },
|
|
522
|
-
body: JSON.stringify({ taskId: task.frontmatter.id,
|
|
522
|
+
body: JSON.stringify({ taskId: task.frontmatter.id, description: `Run task "${task.frontmatter.name || task.frontmatter.id}"?` }),
|
|
523
523
|
});
|
|
524
524
|
const body = await res.json() as { confirmed?: boolean; error?: string };
|
|
525
525
|
if (typeof body.confirmed !== "boolean") {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { CONFIG_DIR } from "./config.js";
|
|
4
|
+
|
|
5
|
+
const LOCATION_FILE = path.join(CONFIG_DIR, "location-device.json");
|
|
6
|
+
|
|
7
|
+
export interface LocationDevice {
|
|
8
|
+
clientToken: string;
|
|
9
|
+
fcmToken: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getLocationDevice(): LocationDevice | null {
|
|
13
|
+
try {
|
|
14
|
+
if (!fs.existsSync(LOCATION_FILE)) return null;
|
|
15
|
+
const raw = fs.readFileSync(LOCATION_FILE, "utf-8");
|
|
16
|
+
const data = JSON.parse(raw) as LocationDevice;
|
|
17
|
+
if (!data.clientToken || !data.fcmToken) return null;
|
|
18
|
+
return data;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function setLocationDevice(clientToken: string, fcmToken: string): void {
|
|
25
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
+
fs.writeFileSync(LOCATION_FILE, JSON.stringify({ clientToken, fcmToken }, null, 2), "utf-8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function clearLocationDevice(): void {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(LOCATION_FILE)) fs.unlinkSync(LOCATION_FILE);
|
|
32
|
+
} catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { agentTools, agentToolMap, ToolError, type ToolContext } from "./mcp-tools.js";
|
|
3
|
+
|
|
4
|
+
interface JsonRpcRequest {
|
|
5
|
+
jsonrpc: string;
|
|
6
|
+
id?: string | number | null;
|
|
7
|
+
method: string;
|
|
8
|
+
params?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface McpResponse {
|
|
12
|
+
body: object;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Session-to-agent name map with 24h TTL
|
|
17
|
+
const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
const sessionAgents = new Map<string, { agentName: string; expiresAt: number }>();
|
|
19
|
+
|
|
20
|
+
export function getAgentName(sessionId: string): string | undefined {
|
|
21
|
+
const entry = sessionAgents.get(sessionId);
|
|
22
|
+
if (!entry) return undefined;
|
|
23
|
+
if (Date.now() > entry.expiresAt) {
|
|
24
|
+
sessionAgents.delete(sessionId);
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return entry.agentName;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pruneExpiredSessions(): void {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
for (const [id, entry] of sessionAgents) {
|
|
33
|
+
if (now > entry.expiresAt) sessionAgents.delete(id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rpcError(id: string | number | null, code: number, message: string): object {
|
|
38
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rpcResult(id: string | number | null, result: unknown): object {
|
|
42
|
+
return { jsonrpc: "2.0", id, result };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function handleMcpRequest(body: string, sessionId: string | undefined, ctx: ToolContext): Promise<McpResponse> {
|
|
46
|
+
let req: JsonRpcRequest;
|
|
47
|
+
try {
|
|
48
|
+
req = JSON.parse(body);
|
|
49
|
+
} catch {
|
|
50
|
+
return { body: rpcError(null, -32700, "Parse error") };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const id = req.id ?? null;
|
|
54
|
+
|
|
55
|
+
if (req.jsonrpc !== "2.0") {
|
|
56
|
+
return { body: rpcError(id, -32600, "Invalid Request: missing jsonrpc 2.0") };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const agent = sessionId ? getAgentName(sessionId) : undefined;
|
|
60
|
+
const sid = sessionId?.slice(0, 8) ?? "none";
|
|
61
|
+
const logPrefix = agent ? `[mcp] [${sid}] [${agent}]` : `[mcp] [${sid}]`;
|
|
62
|
+
console.log(`${logPrefix} ${req.method}${req.method === "tools/call" ? ` → ${req.params?.name}` : ""}`);
|
|
63
|
+
|
|
64
|
+
switch (req.method) {
|
|
65
|
+
case "initialize": {
|
|
66
|
+
const newSessionId = randomUUID();
|
|
67
|
+
const clientInfo = req.params?.clientInfo as { name?: string; version?: string } | undefined;
|
|
68
|
+
const agentName = clientInfo
|
|
69
|
+
? `${clientInfo.name || "unknown"}${clientInfo.version ? ` ${clientInfo.version}` : ""}`
|
|
70
|
+
: undefined;
|
|
71
|
+
|
|
72
|
+
if (agentName) {
|
|
73
|
+
sessionAgents.set(newSessionId, { agentName, expiresAt: Date.now() + SESSION_TTL_MS });
|
|
74
|
+
pruneExpiredSessions();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`[mcp] [${newSessionId.slice(0, 8)}] Session initialized${agentName ? ` (${agentName})` : ""}`);
|
|
78
|
+
return {
|
|
79
|
+
body: rpcResult(id, {
|
|
80
|
+
protocolVersion: "2025-03-26",
|
|
81
|
+
capabilities: { tools: {} },
|
|
82
|
+
serverInfo: { name: "palmier", version: "1.0.0" },
|
|
83
|
+
}),
|
|
84
|
+
sessionId: newSessionId,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case "tools/list": {
|
|
89
|
+
return {
|
|
90
|
+
body: rpcResult(id, {
|
|
91
|
+
tools: agentTools.map((t) => ({
|
|
92
|
+
name: t.name,
|
|
93
|
+
description: t.description,
|
|
94
|
+
inputSchema: t.inputSchema,
|
|
95
|
+
})),
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case "tools/call": {
|
|
101
|
+
const name = req.params?.name as string | undefined;
|
|
102
|
+
const args = (req.params?.arguments ?? {}) as Record<string, unknown>;
|
|
103
|
+
|
|
104
|
+
if (!name) return { body: rpcError(id, -32602, "Missing params.name") };
|
|
105
|
+
|
|
106
|
+
const tool = agentToolMap.get(name);
|
|
107
|
+
if (!tool) return { body: rpcError(id, -32602, `Unknown tool: ${name}`) };
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await tool.handler(args, ctx);
|
|
111
|
+
console.log(`${logPrefix} tools/call ${name} done:`, JSON.stringify(result).slice(0, 200));
|
|
112
|
+
return {
|
|
113
|
+
body: rpcResult(id, {
|
|
114
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
} catch (err: any) {
|
|
118
|
+
const message = err instanceof ToolError ? err.message : String(err);
|
|
119
|
+
console.error(`${logPrefix} tools/call ${name} error:`, message);
|
|
120
|
+
return {
|
|
121
|
+
body: rpcResult(id, {
|
|
122
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
123
|
+
isError: true,
|
|
124
|
+
}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
default:
|
|
130
|
+
console.warn(`${logPrefix} Unknown method: ${req.method}`);
|
|
131
|
+
return { body: rpcError(id, -32601, `Method not found: ${req.method}`) };
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/mcp-tools.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { StringCodec, type NatsConnection } from "nats";
|
|
2
|
+
import { registerPending } from "./pending-requests.js";
|
|
3
|
+
import { getLocationDevice } from "./location-device.js";
|
|
4
|
+
import type { HostConfig } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export class ToolError extends Error {
|
|
7
|
+
constructor(message: string, public statusCode: number = 500) {
|
|
8
|
+
super(message);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ToolContext {
|
|
13
|
+
config: HostConfig;
|
|
14
|
+
nc: NatsConnection | undefined;
|
|
15
|
+
publishEvent: (id: string, payload: Record<string, unknown>) => Promise<void>;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
agentName?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ToolDefinition {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
inputSchema: object;
|
|
24
|
+
handler: (args: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const notifyTool: ToolDefinition = {
|
|
28
|
+
name: "notify",
|
|
29
|
+
description: "Send a push notification to the user's device.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
title: { type: "string", description: "Notification title" },
|
|
34
|
+
body: { type: "string", description: "Notification body" },
|
|
35
|
+
},
|
|
36
|
+
required: ["title", "body"],
|
|
37
|
+
},
|
|
38
|
+
async handler(args, ctx) {
|
|
39
|
+
const { title, body } = args as { title: string; body: string };
|
|
40
|
+
if (!title || !body) throw new ToolError("title and body are required", 400);
|
|
41
|
+
if (!ctx.nc) throw new ToolError("NATS not connected — push notifications require server mode", 503);
|
|
42
|
+
|
|
43
|
+
const sc = StringCodec();
|
|
44
|
+
const payload: Record<string, string> = { hostId: ctx.config.hostId, title, body };
|
|
45
|
+
if (ctx.sessionId) payload.session_id = ctx.sessionId;
|
|
46
|
+
if (ctx.agentName) payload.agent_name = ctx.agentName;
|
|
47
|
+
const subject = `host.${ctx.config.hostId}.push.send`;
|
|
48
|
+
const reply = await ctx.nc.request(subject, sc.encode(JSON.stringify(payload)), { timeout: 15_000 });
|
|
49
|
+
const result = JSON.parse(sc.decode(reply.data)) as { ok?: boolean; error?: string };
|
|
50
|
+
|
|
51
|
+
if (result.ok) return { ok: true };
|
|
52
|
+
throw new ToolError(result.error ?? "Push notification failed", 502);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const requestInputTool: ToolDefinition = {
|
|
57
|
+
name: "request-input",
|
|
58
|
+
description: "Request input from the user. The request blocks until the user responds.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
description: { type: "string", description: "Context or heading for the input request" },
|
|
63
|
+
questions: {
|
|
64
|
+
type: "array",
|
|
65
|
+
items: { type: "string" },
|
|
66
|
+
description: "Questions to present to the user",
|
|
67
|
+
minItems: 1,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
required: ["questions"],
|
|
71
|
+
},
|
|
72
|
+
async handler(args, ctx) {
|
|
73
|
+
const { description, questions } = args as { description?: string; questions: string[] };
|
|
74
|
+
if (!questions?.length) throw new ToolError("questions is required", 400);
|
|
75
|
+
|
|
76
|
+
const pendingPromise = registerPending(ctx.sessionId, "input", questions);
|
|
77
|
+
|
|
78
|
+
await ctx.publishEvent("_input", {
|
|
79
|
+
event_type: "input-request",
|
|
80
|
+
host_id: ctx.config.hostId,
|
|
81
|
+
session_id: ctx.sessionId,
|
|
82
|
+
agent_name: ctx.agentName,
|
|
83
|
+
description,
|
|
84
|
+
input_questions: questions,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const response = await pendingPromise;
|
|
88
|
+
|
|
89
|
+
if (response.length === 1 && response[0] === "aborted") {
|
|
90
|
+
await ctx.publishEvent("_input", { event_type: "input-resolved", host_id: ctx.config.hostId, session_id: ctx.sessionId, status: "aborted" });
|
|
91
|
+
return { aborted: true };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await ctx.publishEvent("_input", { event_type: "input-resolved", host_id: ctx.config.hostId, session_id: ctx.sessionId, status: "provided" });
|
|
95
|
+
return { values: response };
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const requestConfirmationTool: ToolDefinition = {
|
|
100
|
+
name: "request-confirmation",
|
|
101
|
+
description: "Request confirmation from the user. The request blocks until the user confirms or aborts.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
description: { type: "string", description: "What the user is confirming" },
|
|
106
|
+
},
|
|
107
|
+
required: ["description"],
|
|
108
|
+
},
|
|
109
|
+
async handler(args, ctx) {
|
|
110
|
+
const { description } = args as { description: string };
|
|
111
|
+
if (!description) throw new ToolError("description is required", 400);
|
|
112
|
+
|
|
113
|
+
const pendingPromise = registerPending(ctx.sessionId, "confirmation");
|
|
114
|
+
|
|
115
|
+
await ctx.publishEvent("_confirm", {
|
|
116
|
+
event_type: "confirm-request",
|
|
117
|
+
host_id: ctx.config.hostId,
|
|
118
|
+
session_id: ctx.sessionId,
|
|
119
|
+
agent_name: ctx.agentName,
|
|
120
|
+
description,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const response = await pendingPromise;
|
|
124
|
+
const confirmed = response[0] === "confirmed";
|
|
125
|
+
|
|
126
|
+
await ctx.publishEvent("_confirm", {
|
|
127
|
+
event_type: "confirm-resolved",
|
|
128
|
+
host_id: ctx.config.hostId,
|
|
129
|
+
session_id: ctx.sessionId,
|
|
130
|
+
status: confirmed ? "confirmed" : "aborted",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return { confirmed };
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const deviceGeolocationTool: ToolDefinition = {
|
|
138
|
+
name: "device-geolocation",
|
|
139
|
+
description: "Get the GPS location of the user's mobile device. Blocks until the device responds (up to 30 seconds).",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {},
|
|
143
|
+
},
|
|
144
|
+
async handler(_args, ctx) {
|
|
145
|
+
if (!ctx.nc) throw new ToolError("Not connected to server (NATS unavailable)", 503);
|
|
146
|
+
|
|
147
|
+
const locDevice = getLocationDevice();
|
|
148
|
+
if (!locDevice) throw new ToolError("No device has location access enabled", 400);
|
|
149
|
+
|
|
150
|
+
const sc = StringCodec();
|
|
151
|
+
|
|
152
|
+
const ackReply = await ctx.nc.request(
|
|
153
|
+
`host.${ctx.config.hostId}.fcm.geolocation`,
|
|
154
|
+
sc.encode(JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.sessionId, fcmToken: locDevice.fcmToken })),
|
|
155
|
+
{ timeout: 5_000 },
|
|
156
|
+
);
|
|
157
|
+
const ack = JSON.parse(sc.decode(ackReply.data)) as { ok?: boolean; error?: string };
|
|
158
|
+
if (ack.error) throw new ToolError(ack.error, 502);
|
|
159
|
+
|
|
160
|
+
const locationPromise = new Promise<string>((resolve, reject) => {
|
|
161
|
+
const sub = ctx.nc!.subscribe(`host.${ctx.config.hostId}.geolocation.${ctx.sessionId}`, { max: 1 });
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
sub.unsubscribe();
|
|
164
|
+
reject(new ToolError("Device did not respond within 30 seconds", 504));
|
|
165
|
+
}, 30_000);
|
|
166
|
+
|
|
167
|
+
(async () => {
|
|
168
|
+
for await (const msg of sub) {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
resolve(sc.decode(msg.data));
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const locationData = JSON.parse(await locationPromise);
|
|
176
|
+
if (locationData.error) return { error: locationData.error };
|
|
177
|
+
return locationData;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool];
|
|
182
|
+
export const agentToolMap = new Map<string, ToolDefinition>(agentTools.map((t) => [t.name, t]));
|
package/src/rpc-handler.ts
CHANGED
|
@@ -13,6 +13,7 @@ import crossSpawn from "cross-spawn";
|
|
|
13
13
|
import { getAgent } from "./agents/agent.js";
|
|
14
14
|
import { validateClient } from "./client-store.js";
|
|
15
15
|
import { publishHostEvent } from "./events.js";
|
|
16
|
+
import { getLocationDevice, setLocationDevice, clearLocationDevice } from "./location-device.js";
|
|
16
17
|
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
17
18
|
import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
|
|
18
19
|
import type { HostConfig, ParsedTask, RpcMessage, ConversationMessage } from "./types.js";
|
|
@@ -166,27 +167,29 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
166
167
|
body: task.body,
|
|
167
168
|
status: status ? {
|
|
168
169
|
...status,
|
|
169
|
-
...(pending?.type === "confirmation" ? { pending_confirmation: true } : {}),
|
|
170
170
|
...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
|
|
171
|
-
...(pending?.type === "input" ? { pending_input: pending.params } : {}),
|
|
172
171
|
} : undefined,
|
|
173
172
|
};
|
|
174
173
|
}
|
|
175
174
|
|
|
176
175
|
async function handleRpc(request: RpcMessage): Promise<unknown> {
|
|
177
|
-
// Client token validation: skip for trusted localhost requests
|
|
178
|
-
|
|
176
|
+
// Client token validation: skip for trusted localhost requests and
|
|
177
|
+
// task.user_input (server-originated push responses; gated by getPending instead)
|
|
178
|
+
const skipAuth = request.method === "task.user_input";
|
|
179
|
+
if (!skipAuth && !request.localhost && (!request.clientToken || !validateClient(request.clientToken))) {
|
|
179
180
|
return { error: "Unauthorized" };
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
switch (request.method) {
|
|
183
184
|
case "task.list": {
|
|
184
185
|
const tasks = listTasks(config.projectRoot);
|
|
186
|
+
const locDevice = getLocationDevice();
|
|
185
187
|
return {
|
|
186
188
|
tasks: tasks.map((task) => flattenTask(task)),
|
|
187
189
|
agents: config.agents ?? [],
|
|
188
190
|
version: currentVersion,
|
|
189
191
|
host_platform: process.platform,
|
|
192
|
+
location_client_token: locDevice?.clientToken ?? null,
|
|
190
193
|
};
|
|
191
194
|
}
|
|
192
195
|
|
|
@@ -382,15 +385,10 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
382
385
|
const runTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
383
386
|
const platform = getPlatform();
|
|
384
387
|
|
|
385
|
-
//
|
|
388
|
+
// If the task is already running, kill the stale process and start fresh
|
|
386
389
|
if (platform.isTaskRunning(params.id)) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (currentStatus?.running_state !== "started") {
|
|
390
|
-
writeTaskStatus(runTaskDir, { running_state: "started", time_stamp: Date.now() });
|
|
391
|
-
await publishHostEvent(nc, config.hostId, params.id, { event_type: "running-state", running_state: "started" });
|
|
392
|
-
}
|
|
393
|
-
return { error: "Task is already running" };
|
|
390
|
+
console.log(`[task.run] Task ${params.id} is already running, killing stale process`);
|
|
391
|
+
await platform.stopTask(params.id);
|
|
394
392
|
}
|
|
395
393
|
|
|
396
394
|
// Create initial result file so it appears in runs list immediately
|
|
@@ -445,7 +443,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
445
443
|
const child = crossSpawn(cmd, cmdArgs, {
|
|
446
444
|
cwd: followupRunDir,
|
|
447
445
|
stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
448
|
-
env: { ...process.env, ...followupAgentEnv
|
|
446
|
+
env: { ...process.env, ...followupAgentEnv },
|
|
449
447
|
windowsHide: true,
|
|
450
448
|
});
|
|
451
449
|
if (stdin != null) child.stdin!.end(stdin);
|
|
@@ -582,9 +580,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
582
580
|
return {
|
|
583
581
|
task_id: params.id,
|
|
584
582
|
...status,
|
|
585
|
-
...(pending?.type === "confirmation" ? { pending_confirmation: true } : {}),
|
|
586
583
|
...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
|
|
587
|
-
...(pending?.type === "input" ? { pending_input: pending.params } : {}),
|
|
588
584
|
};
|
|
589
585
|
}
|
|
590
586
|
|
|
@@ -695,6 +691,19 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
695
691
|
return { ok: true };
|
|
696
692
|
}
|
|
697
693
|
|
|
694
|
+
case "device.location.enable": {
|
|
695
|
+
const params = request.params as { fcmToken: string };
|
|
696
|
+
if (!params.fcmToken) return { error: "fcmToken is required" };
|
|
697
|
+
const clientToken = request.clientToken ?? "";
|
|
698
|
+
setLocationDevice(clientToken, params.fcmToken);
|
|
699
|
+
return { ok: true };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
case "device.location.disable": {
|
|
703
|
+
clearLocationDevice();
|
|
704
|
+
return { ok: true };
|
|
705
|
+
}
|
|
706
|
+
|
|
698
707
|
default:
|
|
699
708
|
return { error: `Unknown method: ${request.method}` };
|
|
700
709
|
}
|