palmier 0.8.0 → 0.8.3
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/CLAUDE.md +13 -0
- package/README.md +11 -11
- package/dist/agents/agent.d.ts +0 -4
- package/dist/agents/claude.js +1 -1
- package/dist/agents/codex.js +2 -2
- package/dist/agents/cursor.js +1 -1
- package/dist/agents/deepagents.js +1 -1
- package/dist/agents/gemini.js +3 -2
- package/dist/agents/goose.js +1 -1
- package/dist/agents/hermes.js +1 -1
- package/dist/agents/kiro.js +1 -1
- package/dist/agents/opencode.js +1 -1
- package/dist/agents/qoder.js +1 -1
- package/dist/agents/shared-prompt.d.ts +0 -3
- package/dist/agents/shared-prompt.js +0 -3
- package/dist/app-registry.d.ts +10 -0
- package/dist/app-registry.js +44 -0
- package/dist/commands/info.d.ts +0 -3
- package/dist/commands/info.js +0 -5
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.js +2 -11
- package/dist/commands/pair.d.ts +1 -4
- package/dist/commands/pair.js +1 -12
- package/dist/commands/restart.d.ts +0 -3
- package/dist/commands/restart.js +0 -3
- package/dist/commands/run.d.ts +1 -14
- package/dist/commands/run.js +18 -61
- package/dist/commands/serve.d.ts +0 -3
- package/dist/commands/serve.js +33 -27
- package/dist/config.d.ts +0 -8
- package/dist/config.js +0 -8
- package/dist/device-capabilities.d.ts +1 -1
- package/dist/event-queues.d.ts +6 -21
- package/dist/event-queues.js +6 -21
- package/dist/events.d.ts +0 -6
- package/dist/events.js +1 -9
- package/dist/index.js +0 -1
- package/dist/mcp-handler.js +1 -2
- package/dist/mcp-tools.d.ts +0 -3
- package/dist/mcp-tools.js +14 -18
- package/dist/nats-client.d.ts +0 -3
- package/dist/nats-client.js +1 -4
- package/dist/pending-requests.d.ts +4 -18
- package/dist/pending-requests.js +4 -18
- package/dist/platform/index.d.ts +1 -4
- package/dist/platform/index.js +1 -4
- package/dist/platform/linux.d.ts +3 -9
- package/dist/platform/linux.js +9 -20
- package/dist/platform/platform.d.ts +1 -4
- package/dist/platform/windows.d.ts +2 -5
- package/dist/platform/windows.js +19 -39
- package/dist/pwa/assets/index-B0F9mtid.css +1 -0
- package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
- package/dist/pwa/assets/{web-CF-N8Di6.js → web-C6lkQj9J.js} +1 -1
- package/dist/pwa/assets/{web-BpM3fNCn.js → web-Z1623me-.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.d.ts +0 -6
- package/dist/rpc-handler.js +19 -48
- package/dist/spawn-command.d.ts +10 -25
- package/dist/spawn-command.js +7 -15
- package/dist/task.d.ts +6 -64
- package/dist/task.js +7 -70
- package/dist/transports/http-transport.d.ts +0 -4
- package/dist/transports/http-transport.js +6 -28
- package/dist/transports/nats-transport.d.ts +0 -4
- package/dist/transports/nats-transport.js +3 -9
- package/dist/types.d.ts +3 -7
- package/dist/update-checker.d.ts +1 -4
- package/dist/update-checker.js +2 -5
- package/package.json +1 -1
- package/palmier-server/README.md +1 -1
- package/palmier-server/pwa/src/App.css +170 -20
- package/palmier-server/pwa/src/App.tsx +15 -1
- package/palmier-server/pwa/src/components/HostMenu.tsx +282 -473
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
- package/palmier-server/pwa/src/components/SessionsView.tsx +57 -25
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
- package/palmier-server/pwa/src/components/TaskCard.tsx +12 -4
- package/palmier-server/pwa/src/components/TaskForm.tsx +230 -33
- package/palmier-server/pwa/src/components/TasksView.tsx +5 -0
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +66 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
- package/palmier-server/pwa/src/pages/PairHost.tsx +18 -2
- package/palmier-server/pwa/src/types.ts +1 -1
- package/palmier-server/server/src/index.ts +7 -7
- package/palmier-server/server/src/routes/device.ts +4 -4
- package/palmier-server/spec.md +47 -6
- package/src/agents/agent.ts +0 -4
- package/src/agents/claude.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/cursor.ts +1 -1
- package/src/agents/deepagents.ts +1 -1
- package/src/agents/gemini.ts +3 -2
- package/src/agents/goose.ts +1 -1
- package/src/agents/hermes.ts +1 -1
- package/src/agents/kiro.ts +1 -1
- package/src/agents/opencode.ts +1 -1
- package/src/agents/qoder.ts +1 -1
- package/src/agents/shared-prompt.ts +0 -3
- package/src/app-registry.ts +52 -0
- package/src/commands/info.ts +0 -5
- package/src/commands/init.ts +2 -11
- package/src/commands/pair.ts +1 -12
- package/src/commands/restart.ts +0 -3
- package/src/commands/run.ts +18 -65
- package/src/commands/serve.ts +31 -27
- package/src/config.ts +0 -8
- package/src/device-capabilities.ts +4 -3
- package/src/event-queues.ts +6 -21
- package/src/events.ts +1 -9
- package/src/index.ts +0 -1
- package/src/mcp-handler.ts +1 -2
- package/src/mcp-tools.ts +14 -20
- package/src/nats-client.ts +1 -4
- package/src/pending-requests.ts +4 -18
- package/src/platform/index.ts +1 -4
- package/src/platform/linux.ts +9 -20
- package/src/platform/platform.ts +1 -4
- package/src/platform/windows.ts +19 -40
- package/src/rpc-handler.ts +20 -48
- package/src/spawn-command.ts +11 -27
- package/src/task.ts +7 -70
- package/src/transports/http-transport.ts +6 -39
- package/src/transports/nats-transport.ts +3 -9
- package/src/types.ts +3 -10
- package/src/update-checker.ts +2 -5
- package/test/task-parsing.test.ts +2 -3
- package/test/windows-xml.test.ts +11 -12
- package/dist/pwa/assets/index-FP1Mipr6.js +0 -120
- package/dist/pwa/assets/index-bLTn8zBj.css +0 -1
package/dist/task.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
4
|
-
/**
|
|
5
|
-
* Parse a TASK.md file from the given task directory.
|
|
6
|
-
*/
|
|
7
4
|
export function parseTaskFile(taskDir) {
|
|
8
5
|
const filePath = path.join(taskDir, "TASK.md");
|
|
9
6
|
if (!fs.existsSync(filePath)) {
|
|
@@ -12,9 +9,6 @@ export function parseTaskFile(taskDir) {
|
|
|
12
9
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
13
10
|
return parseTaskContent(content);
|
|
14
11
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Parse TASK.md content string into frontmatter + body.
|
|
17
|
-
*/
|
|
18
12
|
export function parseTaskContent(content) {
|
|
19
13
|
const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
20
14
|
const match = content.match(fmRegex);
|
|
@@ -30,10 +24,6 @@ export function parseTaskContent(content) {
|
|
|
30
24
|
frontmatter.schedule_enabled ??= true;
|
|
31
25
|
return { frontmatter };
|
|
32
26
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Write a TASK.md file to the given task directory.
|
|
35
|
-
* Creates the directory if it doesn't exist.
|
|
36
|
-
*/
|
|
37
27
|
export function writeTaskFile(taskDir, task) {
|
|
38
28
|
fs.mkdirSync(taskDir, { recursive: true });
|
|
39
29
|
const yamlStr = stringifyYaml(task.frontmatter).trim();
|
|
@@ -41,17 +31,10 @@ export function writeTaskFile(taskDir, task) {
|
|
|
41
31
|
const filePath = path.join(taskDir, "TASK.md");
|
|
42
32
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
43
33
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Append a task ID to the project-level tasks.jsonl file.
|
|
46
|
-
*/
|
|
47
34
|
export function appendTaskList(projectRoot, taskId) {
|
|
48
35
|
const listPath = path.join(projectRoot, "tasks.jsonl");
|
|
49
36
|
fs.appendFileSync(listPath, JSON.stringify({ task_id: taskId }) + "\n", "utf-8");
|
|
50
37
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Remove a task ID from the project-level tasks.jsonl file.
|
|
53
|
-
* Returns true if the entry was found and removed.
|
|
54
|
-
*/
|
|
55
38
|
export function removeFromTaskList(projectRoot, taskId) {
|
|
56
39
|
const listPath = path.join(projectRoot, "tasks.jsonl");
|
|
57
40
|
if (!fs.existsSync(listPath))
|
|
@@ -75,9 +58,6 @@ export function removeFromTaskList(projectRoot, taskId) {
|
|
|
75
58
|
fs.writeFileSync(listPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
|
|
76
59
|
return true;
|
|
77
60
|
}
|
|
78
|
-
/**
|
|
79
|
-
* List all tasks referenced in tasks.jsonl.
|
|
80
|
-
*/
|
|
81
61
|
export function listTasks(projectRoot) {
|
|
82
62
|
const listPath = path.join(projectRoot, "tasks.jsonl");
|
|
83
63
|
if (!fs.existsSync(listPath))
|
|
@@ -102,23 +82,13 @@ export function listTasks(projectRoot) {
|
|
|
102
82
|
}
|
|
103
83
|
return tasks.reverse();
|
|
104
84
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Get the directory path for a task by its ID.
|
|
107
|
-
*/
|
|
108
85
|
export function getTaskDir(projectRoot, taskId) {
|
|
109
86
|
return path.join(projectRoot, "tasks", taskId);
|
|
110
87
|
}
|
|
111
|
-
/**
|
|
112
|
-
* Write task status to status.json in the task directory.
|
|
113
|
-
*/
|
|
114
88
|
export function writeTaskStatus(taskDir, status) {
|
|
115
89
|
const filePath = path.join(taskDir, "status.json");
|
|
116
90
|
fs.writeFileSync(filePath, JSON.stringify(status), "utf-8");
|
|
117
91
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Read task status from status.json in the task directory.
|
|
120
|
-
* Returns undefined if the file doesn't exist.
|
|
121
|
-
*/
|
|
122
92
|
export function readTaskStatus(taskDir) {
|
|
123
93
|
const filePath = path.join(taskDir, "status.json");
|
|
124
94
|
try {
|
|
@@ -128,10 +98,7 @@ export function readTaskStatus(taskDir) {
|
|
|
128
98
|
return undefined;
|
|
129
99
|
}
|
|
130
100
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Create a run directory with an initial TASKRUN.md file.
|
|
133
|
-
* Returns the run ID (timestamp string used as directory name).
|
|
134
|
-
*/
|
|
101
|
+
/** Returns the run ID (timestamp string used as directory name). */
|
|
135
102
|
export function createRunDir(taskDir, taskName, startTime, agent) {
|
|
136
103
|
const runId = String(startTime);
|
|
137
104
|
const runDir = path.join(taskDir, runId);
|
|
@@ -141,15 +108,9 @@ export function createRunDir(taskDir, taskName, startTime, agent) {
|
|
|
141
108
|
fs.writeFileSync(path.join(runDir, "TASKRUN.md"), content, "utf-8");
|
|
142
109
|
return runId;
|
|
143
110
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Get the path to a run directory.
|
|
146
|
-
*/
|
|
147
111
|
export function getRunDir(taskDir, runId) {
|
|
148
112
|
return path.join(taskDir, runId);
|
|
149
113
|
}
|
|
150
|
-
/**
|
|
151
|
-
* Append a conversation message to a run's TASKRUN.md file.
|
|
152
|
-
*/
|
|
153
114
|
export function appendRunMessage(taskDir, runId, msg) {
|
|
154
115
|
const attrs = [`role="${msg.role}"`, `time="${msg.time}"`];
|
|
155
116
|
if (msg.type)
|
|
@@ -160,10 +121,6 @@ export function appendRunMessage(taskDir, runId, msg) {
|
|
|
160
121
|
const entry = `${delimiter}\n\n${msg.content}\n\n`;
|
|
161
122
|
fs.appendFileSync(path.join(taskDir, runId, "TASKRUN.md"), entry, "utf-8");
|
|
162
123
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Begin a streaming assistant message — writes the delimiter only.
|
|
165
|
-
* Returns a writer that appends content chunks and finalizes the message.
|
|
166
|
-
*/
|
|
167
124
|
export function beginStreamingMessage(taskDir, runId, time) {
|
|
168
125
|
const filePath = path.join(taskDir, runId, "TASKRUN.md");
|
|
169
126
|
const delimiter = `<!-- palmier:message role="assistant" time="${time}" -->`;
|
|
@@ -184,7 +141,7 @@ export class StreamingMessageWriter {
|
|
|
184
141
|
fs.appendFileSync(this.filePath, "\n\n", "utf-8");
|
|
185
142
|
if (attachments?.length) {
|
|
186
143
|
const raw = fs.readFileSync(this.filePath, "utf-8");
|
|
187
|
-
//
|
|
144
|
+
// spliceUserMessage may have created a newer assistant delimiter.
|
|
188
145
|
const pattern = /<!-- palmier:message role="assistant" time="\d+" -->/g;
|
|
189
146
|
let lastMatch = null;
|
|
190
147
|
let m;
|
|
@@ -200,32 +157,23 @@ export class StreamingMessageWriter {
|
|
|
200
157
|
}
|
|
201
158
|
}
|
|
202
159
|
/**
|
|
203
|
-
* Splice a user message into a running assistant stream
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* write() is just appendFileSync, so subsequent chunks land in
|
|
208
|
-
* the new assistant block.
|
|
160
|
+
* Splice a user message into a running assistant stream: close the current
|
|
161
|
+
* assistant block, write the user message, open a new assistant block. Direct
|
|
162
|
+
* appends only, so an existing StreamingMessageWriter keeps working — its
|
|
163
|
+
* subsequent chunks land in the new block.
|
|
209
164
|
*/
|
|
210
165
|
export function spliceUserMessage(taskDir, runId, userMsg,
|
|
211
166
|
/** Optional text to append to the current assistant block before ending it. */
|
|
212
167
|
assistantAppend) {
|
|
213
168
|
const filePath = path.join(taskDir, runId, "TASKRUN.md");
|
|
214
|
-
// 1. Optionally append to the current assistant block (e.g. the input questions)
|
|
215
169
|
if (assistantAppend) {
|
|
216
170
|
fs.appendFileSync(filePath, assistantAppend, "utf-8");
|
|
217
171
|
}
|
|
218
|
-
// 2. End the current assistant block
|
|
219
172
|
fs.appendFileSync(filePath, "\n\n", "utf-8");
|
|
220
|
-
// 3. Write the user message
|
|
221
173
|
appendRunMessage(taskDir, runId, userMsg);
|
|
222
|
-
// 4. Open a new assistant block for subsequent agent output
|
|
223
174
|
const delimiter = `<!-- palmier:message role="assistant" time="${Date.now()}" -->`;
|
|
224
175
|
fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
|
|
225
176
|
}
|
|
226
|
-
/**
|
|
227
|
-
* Read conversation messages from a run's TASKRUN.md file.
|
|
228
|
-
*/
|
|
229
177
|
export function readRunMessages(taskDir, runId) {
|
|
230
178
|
const raw = fs.readFileSync(path.join(taskDir, runId, "TASKRUN.md"), "utf-8");
|
|
231
179
|
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
@@ -257,17 +205,10 @@ export function readRunMessages(taskDir, runId) {
|
|
|
257
205
|
}
|
|
258
206
|
return messages;
|
|
259
207
|
}
|
|
260
|
-
/**
|
|
261
|
-
* Append a history entry to the project-level history.jsonl file.
|
|
262
|
-
*/
|
|
263
208
|
export function appendHistory(projectRoot, entry) {
|
|
264
209
|
const historyPath = path.join(projectRoot, "history.jsonl");
|
|
265
210
|
fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
266
211
|
}
|
|
267
|
-
/**
|
|
268
|
-
* Delete a history entry and its associated run directory.
|
|
269
|
-
* Returns true if the entry was found and removed.
|
|
270
|
-
*/
|
|
271
212
|
export function deleteHistoryEntry(projectRoot, taskId, runId) {
|
|
272
213
|
const historyPath = path.join(projectRoot, "history.jsonl");
|
|
273
214
|
if (!fs.existsSync(historyPath))
|
|
@@ -289,17 +230,13 @@ export function deleteHistoryEntry(projectRoot, taskId, runId) {
|
|
|
289
230
|
if (!found)
|
|
290
231
|
return false;
|
|
291
232
|
fs.writeFileSync(historyPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
|
|
292
|
-
// Delete the run directory
|
|
293
233
|
const runDir = path.join(projectRoot, "tasks", taskId, runId);
|
|
294
234
|
if (fs.existsSync(runDir)) {
|
|
295
235
|
fs.rmSync(runDir, { recursive: true, force: true });
|
|
296
236
|
}
|
|
297
237
|
return true;
|
|
298
238
|
}
|
|
299
|
-
/**
|
|
300
|
-
* Read history entries from history.jsonl with pagination.
|
|
301
|
-
* Returns entries sorted most-recent-first.
|
|
302
|
-
*/
|
|
239
|
+
/** Returns entries most-recent-first. */
|
|
303
240
|
export function readHistory(projectRoot, opts) {
|
|
304
241
|
const historyPath = path.join(projectRoot, "history.jsonl");
|
|
305
242
|
if (!fs.existsSync(historyPath))
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { type NatsConnection } from "nats";
|
|
2
2
|
import type { HostConfig, RpcMessage } from "../types.js";
|
|
3
3
|
export declare function detectLanIp(): string;
|
|
4
|
-
/**
|
|
5
|
-
* Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
|
|
6
|
-
* localhost-only agent endpoints (notify, request-input, confirmation, permission).
|
|
7
|
-
*/
|
|
8
4
|
export declare function startHttpTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>, port: number, nc: NatsConnection | undefined, pairingCode?: string, onReady?: () => void): Promise<void>;
|
|
9
5
|
//# sourceMappingURL=http-transport.d.ts.map
|
|
@@ -29,21 +29,17 @@ function guessContentType(urlPath) {
|
|
|
29
29
|
const ext = urlPath.match(/\.[^.]+$/)?.[0] ?? "";
|
|
30
30
|
return CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Read a PWA asset from the bundled pwa/ directory, caching in memory.
|
|
34
|
-
* Returns null if the file does not exist.
|
|
35
|
-
*/
|
|
36
32
|
function getAsset(urlPath) {
|
|
37
33
|
const cached = assetCache.get(urlPath);
|
|
38
34
|
if (cached)
|
|
39
35
|
return cached;
|
|
40
36
|
const filePath = path.join(PWA_DIR, urlPath === "/" ? "index.html" : urlPath);
|
|
41
|
-
// Prevent path traversal
|
|
37
|
+
// Prevent path traversal.
|
|
42
38
|
if (!filePath.startsWith(PWA_DIR))
|
|
43
39
|
return null;
|
|
44
40
|
try {
|
|
45
41
|
let data = fs.readFileSync(filePath);
|
|
46
|
-
//
|
|
42
|
+
// Marker lets the PWA detect it's served by palmier.
|
|
47
43
|
if (urlPath === "/") {
|
|
48
44
|
const html = data.toString("utf-8").replace("</head>", "<script>window.__PALMIER_SERVE__=true</script></head>");
|
|
49
45
|
data = Buffer.from(html, "utf-8");
|
|
@@ -68,10 +64,6 @@ export function detectLanIp() {
|
|
|
68
64
|
}
|
|
69
65
|
return "127.0.0.1";
|
|
70
66
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
|
|
73
|
-
* localhost-only agent endpoints (notify, request-input, confirmation, permission).
|
|
74
|
-
*/
|
|
75
67
|
export async function startHttpTransport(config, handleRpc, port, nc, pairingCode, onReady) {
|
|
76
68
|
const sseClients = new Set();
|
|
77
69
|
const mcpStreams = new Map();
|
|
@@ -96,7 +88,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
96
88
|
for (const resource of agentResources) {
|
|
97
89
|
resource.subscribe(() => broadcastResourceUpdated(resource.uri));
|
|
98
90
|
}
|
|
99
|
-
// If a pairing code is provided, pre-register it
|
|
100
91
|
if (pairingCode) {
|
|
101
92
|
const EXPIRY_MS = 24 * 60 * 60 * 1000;
|
|
102
93
|
const timer = setTimeout(() => { pendingPairs.delete(pairingCode); }, EXPIRY_MS);
|
|
@@ -136,9 +127,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
136
127
|
const addr = req.socket.remoteAddress;
|
|
137
128
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
138
129
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Publish an event via NATS and SSE.
|
|
141
|
-
*/
|
|
142
130
|
async function publishEvent(taskId, payload) {
|
|
143
131
|
const sc = StringCodec();
|
|
144
132
|
const subject = `host-event.${config.hostId}.${taskId}`;
|
|
@@ -153,7 +141,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
153
141
|
const server = http.createServer(async (req, res) => {
|
|
154
142
|
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
155
143
|
const pathname = url.pathname;
|
|
156
|
-
// ── MCP streamable HTTP endpoint ──────────────────────────────────
|
|
157
144
|
if (req.method === "POST" && pathname === "/mcp") {
|
|
158
145
|
if (!isLocalhost(req)) {
|
|
159
146
|
sendJson(res, 403, { error: "localhost only" });
|
|
@@ -168,7 +155,7 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
168
155
|
res.setHeader("Mcp-Session-Id", result.sessionId);
|
|
169
156
|
}
|
|
170
157
|
if (result.stream && sessionId) {
|
|
171
|
-
// Keep response open as SSE
|
|
158
|
+
// Keep the response open as SSE for server-initiated notifications.
|
|
172
159
|
res.writeHead(200, {
|
|
173
160
|
"Content-Type": "text/event-stream",
|
|
174
161
|
"Cache-Control": "no-cache",
|
|
@@ -192,7 +179,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
192
179
|
}
|
|
193
180
|
return;
|
|
194
181
|
}
|
|
195
|
-
// ── Auto-generated REST endpoints from MCP tool registry ──────────
|
|
196
182
|
if (req.method === "POST" && agentToolMap.has(pathname.slice(1))) {
|
|
197
183
|
if (!isLocalhost(req)) {
|
|
198
184
|
sendJson(res, 403, { error: "localhost only" });
|
|
@@ -225,7 +211,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
225
211
|
}
|
|
226
212
|
return;
|
|
227
213
|
}
|
|
228
|
-
// ── Auto-generated REST endpoints from MCP resource registry ────
|
|
229
214
|
const matchedResource = req.method === "GET" && agentResources.find((r) => r.restPath === pathname);
|
|
230
215
|
if (matchedResource) {
|
|
231
216
|
if (!isLocalhost(req)) {
|
|
@@ -248,7 +233,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
248
233
|
sendJson(res, 200, result);
|
|
249
234
|
return;
|
|
250
235
|
}
|
|
251
|
-
// ── Event queue pop (used by event-triggered palmier run) ─────────
|
|
252
236
|
if (req.method === "POST" && pathname === "/task-event/pop") {
|
|
253
237
|
if (!isLocalhost(req)) {
|
|
254
238
|
sendJson(res, 403, { error: "localhost only" });
|
|
@@ -262,7 +246,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
262
246
|
sendJson(res, 200, popEvent(taskId));
|
|
263
247
|
return;
|
|
264
248
|
}
|
|
265
|
-
// ── Localhost-only endpoints (no auth) ─────────────────────────────
|
|
266
249
|
if (req.method === "POST" && pathname === "/event") {
|
|
267
250
|
if (!isLocalhost(req)) {
|
|
268
251
|
sendJson(res, 403, { error: "localhost only" });
|
|
@@ -315,7 +298,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
315
298
|
}
|
|
316
299
|
return;
|
|
317
300
|
}
|
|
318
|
-
// ── POST /request-permission — held connection ──────────────────────
|
|
319
301
|
if (req.method === "POST" && pathname === "/request-permission") {
|
|
320
302
|
if (!isLocalhost(req)) {
|
|
321
303
|
sendJson(res, 403, { error: "localhost only" });
|
|
@@ -352,7 +334,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
352
334
|
}
|
|
353
335
|
return;
|
|
354
336
|
}
|
|
355
|
-
// ── Public pair endpoint — no auth, PWA posts pairing code here ────────
|
|
356
337
|
if (req.method === "POST" && pathname === "/pair") {
|
|
357
338
|
try {
|
|
358
339
|
const body = await readBody(req);
|
|
@@ -383,8 +364,7 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
383
364
|
}
|
|
384
365
|
return;
|
|
385
366
|
}
|
|
386
|
-
//
|
|
387
|
-
// Skip service worker and manifest — they require HTTPS which LAN mode doesn't use
|
|
367
|
+
// Service worker and manifest require HTTPS, which LAN mode doesn't use.
|
|
388
368
|
const SKIP = new Set(["/registerSW.js", "/service-worker.js", "/manifest.webmanifest"]);
|
|
389
369
|
const isApiRoute = pathname === "/events" || pathname.startsWith("/rpc/");
|
|
390
370
|
if (!isApiRoute) {
|
|
@@ -392,7 +372,7 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
392
372
|
sendJson(res, 404, { error: "Not found" });
|
|
393
373
|
return;
|
|
394
374
|
}
|
|
395
|
-
//
|
|
375
|
+
// Fall back to index.html for SPA routing.
|
|
396
376
|
let asset = getAsset(pathname);
|
|
397
377
|
if (!asset && pathname !== "/") {
|
|
398
378
|
asset = getAsset("/");
|
|
@@ -406,12 +386,11 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
406
386
|
}
|
|
407
387
|
return;
|
|
408
388
|
}
|
|
409
|
-
//
|
|
389
|
+
// Localhost is trusted; all other API callers require a client token.
|
|
410
390
|
if (!isLocalhost(req) && !checkAuth(req)) {
|
|
411
391
|
sendJson(res, 401, { error: "Unauthorized" });
|
|
412
392
|
return;
|
|
413
393
|
}
|
|
414
|
-
// SSE event stream
|
|
415
394
|
if (req.method === "GET" && pathname === "/events") {
|
|
416
395
|
res.writeHead(200, {
|
|
417
396
|
"Content-Type": "text/event-stream",
|
|
@@ -429,7 +408,6 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
|
|
|
429
408
|
});
|
|
430
409
|
return;
|
|
431
410
|
}
|
|
432
|
-
// RPC endpoint: POST /rpc/<method>
|
|
433
411
|
if (req.method === "POST" && pathname.startsWith("/rpc/")) {
|
|
434
412
|
const method = pathname.slice("/rpc/".length);
|
|
435
413
|
if (!method) {
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { type NatsConnection } from "nats";
|
|
2
2
|
import type { HostConfig, RpcMessage } from "../types.js";
|
|
3
|
-
/**
|
|
4
|
-
* Start the NATS transport using an existing connection.
|
|
5
|
-
* Subscribe to RPC subjects and dispatch to handler.
|
|
6
|
-
*/
|
|
7
3
|
export declare function startNatsTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>, nc: NatsConnection): Promise<void>;
|
|
8
4
|
//# sourceMappingURL=nats-transport.d.ts.map
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { StringCodec } from "nats";
|
|
2
|
-
/**
|
|
3
|
-
* Start the NATS transport using an existing connection.
|
|
4
|
-
* Subscribe to RPC subjects and dispatch to handler.
|
|
5
|
-
*/
|
|
6
2
|
export async function startNatsTransport(config, handleRpc, nc) {
|
|
7
3
|
const sc = StringCodec();
|
|
8
4
|
const subject = `host.${config.hostId}.rpc.>`;
|
|
9
5
|
console.log(`[nats] Subscribing to: ${subject}`);
|
|
10
6
|
const sub = nc.subscribe(subject);
|
|
11
|
-
// Graceful shutdown
|
|
12
7
|
const shutdown = async () => {
|
|
13
8
|
console.log("[nats] Shutting down...");
|
|
14
9
|
sub.unsubscribe();
|
|
@@ -18,11 +13,10 @@ export async function startNatsTransport(config, handleRpc, nc) {
|
|
|
18
13
|
process.on("SIGINT", shutdown);
|
|
19
14
|
process.on("SIGTERM", shutdown);
|
|
20
15
|
async function processMessage(msg) {
|
|
21
|
-
//
|
|
16
|
+
// Subject format: ...rpc.<method parts>
|
|
22
17
|
const subjectTokens = msg.subject.split(".");
|
|
23
18
|
const rpcIdx = subjectTokens.indexOf("rpc");
|
|
24
19
|
const method = rpcIdx >= 0 ? subjectTokens.slice(rpcIdx + 1).join(".") : "";
|
|
25
|
-
// Parse params from message body
|
|
26
20
|
let params = {};
|
|
27
21
|
if (msg.data && msg.data.length > 0) {
|
|
28
22
|
const raw = sc.decode(msg.data).trim();
|
|
@@ -39,7 +33,7 @@ export async function startNatsTransport(config, handleRpc, nc) {
|
|
|
39
33
|
}
|
|
40
34
|
}
|
|
41
35
|
}
|
|
42
|
-
//
|
|
36
|
+
// PWA includes the client token in the payload.
|
|
43
37
|
const clientToken = typeof params.clientToken === "string" ? params.clientToken : undefined;
|
|
44
38
|
delete params.clientToken;
|
|
45
39
|
console.log(`[nats] RPC: ${method}`);
|
|
@@ -58,7 +52,7 @@ export async function startNatsTransport(config, handleRpc, nc) {
|
|
|
58
52
|
}
|
|
59
53
|
async function consumeSubscription(subscription) {
|
|
60
54
|
for await (const msg of subscription) {
|
|
61
|
-
//
|
|
55
|
+
// Don't await — heartbeats must keep flowing while RPC runs.
|
|
62
56
|
processMessage(msg);
|
|
63
57
|
}
|
|
64
58
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface HostConfig {
|
|
|
12
12
|
supportsYolo: boolean;
|
|
13
13
|
}>;
|
|
14
14
|
httpPort?: number;
|
|
15
|
+
/** Whether to accept non-localhost HTTP connections. */
|
|
15
16
|
lanEnabled?: boolean;
|
|
16
17
|
}
|
|
17
18
|
export interface TaskFrontmatter {
|
|
@@ -23,8 +24,8 @@ export interface TaskFrontmatter {
|
|
|
23
24
|
* Task schedule.
|
|
24
25
|
* - `crons`: `schedule_values` holds cron expressions (e.g. "0 9 * * *")
|
|
25
26
|
* - `specific_times`: `schedule_values` holds local datetime strings (e.g. "2026-04-20T09:00")
|
|
26
|
-
* - `on_new_notification`: fires on each new Android notification from NATS
|
|
27
|
-
* - `on_new_sms`: fires on each new SMS from NATS
|
|
27
|
+
* - `on_new_notification`: fires on each new Android notification from NATS. Optional `schedule_values` holds a single-entry packageName filter; empty/unset matches any app.
|
|
28
|
+
* - `on_new_sms`: fires on each new SMS from NATS. Optional `schedule_values` holds a single-entry sender filter; compared after normalization (strip spaces/dashes/parens/plus, lowercase). Empty/unset matches any sender.
|
|
28
29
|
*/
|
|
29
30
|
schedule_type?: "crons" | "specific_times" | "on_new_notification" | "on_new_sms";
|
|
30
31
|
schedule_values?: string[];
|
|
@@ -45,11 +46,6 @@ export interface ParsedTask {
|
|
|
45
46
|
* - `failed`: agent exited with an error
|
|
46
47
|
*/
|
|
47
48
|
export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
|
|
48
|
-
/**
|
|
49
|
-
* Persisted to `status.json` in the task directory. Used for crash detection
|
|
50
|
-
* (checkStaleTasks) and abort signalling. Interactive request flows (confirmation,
|
|
51
|
-
* permission, input) are handled via held HTTP connections on the serve daemon.
|
|
52
|
-
*/
|
|
53
49
|
export interface TaskStatus {
|
|
54
50
|
running_state: TaskRunningState;
|
|
55
51
|
time_stamp: number;
|
package/dist/update-checker.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/** True when running from a source checkout (has .git) rather than a global npm install. */
|
|
2
2
|
export declare const isDevBuild: boolean;
|
|
3
3
|
export declare const currentVersion: string;
|
|
4
|
-
/**
|
|
5
|
-
* Run the update and restart the daemon.
|
|
6
|
-
* Returns an error message if the update fails.
|
|
7
|
-
*/
|
|
4
|
+
/** Returns an error message if the update fails. */
|
|
8
5
|
export declare function performUpdate(): Promise<string | null>;
|
|
9
6
|
//# sourceMappingURL=update-checker.d.ts.map
|
package/dist/update-checker.js
CHANGED
|
@@ -9,10 +9,7 @@ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "
|
|
|
9
9
|
/** True when running from a source checkout (has .git) rather than a global npm install. */
|
|
10
10
|
export const isDevBuild = fs.existsSync(path.join(packageRoot, ".git"));
|
|
11
11
|
export const currentVersion = isDevBuild ? `${pkg.version}-dev` : pkg.version;
|
|
12
|
-
/**
|
|
13
|
-
* Run the update and restart the daemon.
|
|
14
|
-
* Returns an error message if the update fails.
|
|
15
|
-
*/
|
|
12
|
+
/** Returns an error message if the update fails. */
|
|
16
13
|
export async function performUpdate() {
|
|
17
14
|
try {
|
|
18
15
|
const { output, exitCode } = await spawnCommand("npm", ["update", "-g", "palmier"], {
|
|
@@ -25,7 +22,7 @@ export async function performUpdate() {
|
|
|
25
22
|
return `Update failed. Please run manually:\nnpm update -g palmier`;
|
|
26
23
|
}
|
|
27
24
|
console.log("[update] Update installed, restarting daemon...");
|
|
28
|
-
//
|
|
25
|
+
// Delay so the RPC response finishes sending first.
|
|
29
26
|
setTimeout(() => {
|
|
30
27
|
getPlatform().restartDaemon().catch((err) => {
|
|
31
28
|
console.error("[update] Restart failed:", err);
|
package/package.json
CHANGED
package/palmier-server/README.md
CHANGED
|
@@ -183,7 +183,7 @@ All endpoints are prefixed with `/api`. No user authentication is required.
|
|
|
183
183
|
- **NATS RPC** — the RPC method is derived from the NATS subject (e.g., `...rpc.task.list` → `task.list`), not the message body. The body contains request parameters plus `clientToken`. All NATS requests go through a centralized `request()` helper in `HostConnectionContext` that handles encoding/decoding and logging.
|
|
184
184
|
- **Pairing** — `palmier pair` (or auto-pair after `palmier init`) generates a 6-char pairing code. The PWA enters the code, which routes to the host via NATS (`pair.<CODE>`) or HTTP (`POST /pair`). The host validates the code and returns a client token.
|
|
185
185
|
- **Task IDs** are generated by the host as UUIDs.
|
|
186
|
-
- **Schedule can be enabled/disabled** — the `schedule_enabled` frontmatter field (default `true`) controls whether
|
|
186
|
+
- **Schedule can be enabled/disabled** — the `schedule_enabled` frontmatter field (default `true`) controls whether the schedule is active. When disabled, timers are removed and device events are ignored for that task, but it can still be run manually. The schedule lives in two flat fields: `schedule_type` (`"crons"`, `"specific_times"`, `"on_new_notification"`, or `"on_new_sms"`) and `schedule_values` (array of cron expressions or local datetime strings — only used by `"crons"` and `"specific_times"`). The two `on_new_*` types have no `schedule_values`; they fire in response to device notifications/SMS relayed over NATS, with the daemon owning a per-task FIFO queue that `palmier run` drains via `POST /task-event/pop`.
|
|
187
187
|
- **Host responses** return flat task objects (frontmatter fields at the top level, not nested) for `task.list`, `task.create`, and `task.update`.
|
|
188
188
|
- **NATS "503"** means "no responders" — the dashboard silently handles this when no host is connected, showing an empty task list instead of an error.
|
|
189
189
|
- **Helmet CSP** is disabled (`contentSecurityPolicy: false`) to allow NATS WebSocket connections and inline Vite scripts during dev.
|