palmier 0.8.1 → 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 +12 -16
- 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-CQxcuDhM.css → index-B0F9mtid.css} +1 -1
- package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
- package/dist/pwa/assets/{web-D7Kq3Nvk.js → web-C6lkQj9J.js} +1 -1
- package/dist/pwa/assets/{web-DOyOiwsW.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 +18 -47
- 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/pwa/src/App.css +165 -20
- package/palmier-server/pwa/src/components/HostMenu.tsx +159 -49
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
- package/palmier-server/pwa/src/components/SessionsView.tsx +57 -31
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
- package/palmier-server/pwa/src/components/TaskForm.tsx +152 -2
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +20 -2
- package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
- package/palmier-server/server/src/index.ts +7 -7
- package/palmier-server/server/src/routes/device.ts +4 -4
- package/palmier-server/spec.md +38 -7
- 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 +3 -2
- 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 +12 -18
- 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 +19 -47
- 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-DQfOEB03.js +0 -120
package/src/task.ts
CHANGED
|
@@ -3,9 +3,6 @@ import * as path from "path";
|
|
|
3
3
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
4
4
|
import type { ParsedTask, TaskFrontmatter, TaskStatus, HistoryEntry, ConversationMessage } from "./types.js";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Parse a TASK.md file from the given task directory.
|
|
8
|
-
*/
|
|
9
6
|
export function parseTaskFile(taskDir: string): ParsedTask {
|
|
10
7
|
const filePath = path.join(taskDir, "TASK.md");
|
|
11
8
|
|
|
@@ -17,9 +14,6 @@ export function parseTaskFile(taskDir: string): ParsedTask {
|
|
|
17
14
|
return parseTaskContent(content);
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
/**
|
|
21
|
-
* Parse TASK.md content string into frontmatter + body.
|
|
22
|
-
*/
|
|
23
17
|
export function parseTaskContent(content: string): ParsedTask {
|
|
24
18
|
const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
25
19
|
const match = content.match(fmRegex);
|
|
@@ -41,10 +35,6 @@ export function parseTaskContent(content: string): ParsedTask {
|
|
|
41
35
|
return { frontmatter };
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
/**
|
|
45
|
-
* Write a TASK.md file to the given task directory.
|
|
46
|
-
* Creates the directory if it doesn't exist.
|
|
47
|
-
*/
|
|
48
38
|
export function writeTaskFile(taskDir: string, task: ParsedTask): void {
|
|
49
39
|
fs.mkdirSync(taskDir, { recursive: true });
|
|
50
40
|
|
|
@@ -55,18 +45,11 @@ export function writeTaskFile(taskDir: string, task: ParsedTask): void {
|
|
|
55
45
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
56
46
|
}
|
|
57
47
|
|
|
58
|
-
/**
|
|
59
|
-
* Append a task ID to the project-level tasks.jsonl file.
|
|
60
|
-
*/
|
|
61
48
|
export function appendTaskList(projectRoot: string, taskId: string): void {
|
|
62
49
|
const listPath = path.join(projectRoot, "tasks.jsonl");
|
|
63
50
|
fs.appendFileSync(listPath, JSON.stringify({ task_id: taskId }) + "\n", "utf-8");
|
|
64
51
|
}
|
|
65
52
|
|
|
66
|
-
/**
|
|
67
|
-
* Remove a task ID from the project-level tasks.jsonl file.
|
|
68
|
-
* Returns true if the entry was found and removed.
|
|
69
|
-
*/
|
|
70
53
|
export function removeFromTaskList(projectRoot: string, taskId: string): boolean {
|
|
71
54
|
const listPath = path.join(projectRoot, "tasks.jsonl");
|
|
72
55
|
if (!fs.existsSync(listPath)) return false;
|
|
@@ -91,9 +74,6 @@ export function removeFromTaskList(projectRoot: string, taskId: string): boolean
|
|
|
91
74
|
return true;
|
|
92
75
|
}
|
|
93
76
|
|
|
94
|
-
/**
|
|
95
|
-
* List all tasks referenced in tasks.jsonl.
|
|
96
|
-
*/
|
|
97
77
|
export function listTasks(projectRoot: string): ParsedTask[] {
|
|
98
78
|
const listPath = path.join(projectRoot, "tasks.jsonl");
|
|
99
79
|
if (!fs.existsSync(listPath)) return [];
|
|
@@ -118,25 +98,15 @@ export function listTasks(projectRoot: string): ParsedTask[] {
|
|
|
118
98
|
return tasks.reverse();
|
|
119
99
|
}
|
|
120
100
|
|
|
121
|
-
/**
|
|
122
|
-
* Get the directory path for a task by its ID.
|
|
123
|
-
*/
|
|
124
101
|
export function getTaskDir(projectRoot: string, taskId: string): string {
|
|
125
102
|
return path.join(projectRoot, "tasks", taskId);
|
|
126
103
|
}
|
|
127
104
|
|
|
128
|
-
/**
|
|
129
|
-
* Write task status to status.json in the task directory.
|
|
130
|
-
*/
|
|
131
105
|
export function writeTaskStatus(taskDir: string, status: TaskStatus): void {
|
|
132
106
|
const filePath = path.join(taskDir, "status.json");
|
|
133
107
|
fs.writeFileSync(filePath, JSON.stringify(status), "utf-8");
|
|
134
108
|
}
|
|
135
109
|
|
|
136
|
-
/**
|
|
137
|
-
* Read task status from status.json in the task directory.
|
|
138
|
-
* Returns undefined if the file doesn't exist.
|
|
139
|
-
*/
|
|
140
110
|
export function readTaskStatus(taskDir: string): TaskStatus | undefined {
|
|
141
111
|
const filePath = path.join(taskDir, "status.json");
|
|
142
112
|
try {
|
|
@@ -146,10 +116,7 @@ export function readTaskStatus(taskDir: string): TaskStatus | undefined {
|
|
|
146
116
|
}
|
|
147
117
|
}
|
|
148
118
|
|
|
149
|
-
/**
|
|
150
|
-
* Create a run directory with an initial TASKRUN.md file.
|
|
151
|
-
* Returns the run ID (timestamp string used as directory name).
|
|
152
|
-
*/
|
|
119
|
+
/** Returns the run ID (timestamp string used as directory name). */
|
|
153
120
|
export function createRunDir(
|
|
154
121
|
taskDir: string,
|
|
155
122
|
taskName: string,
|
|
@@ -165,16 +132,10 @@ export function createRunDir(
|
|
|
165
132
|
return runId;
|
|
166
133
|
}
|
|
167
134
|
|
|
168
|
-
/**
|
|
169
|
-
* Get the path to a run directory.
|
|
170
|
-
*/
|
|
171
135
|
export function getRunDir(taskDir: string, runId: string): string {
|
|
172
136
|
return path.join(taskDir, runId);
|
|
173
137
|
}
|
|
174
138
|
|
|
175
|
-
/**
|
|
176
|
-
* Append a conversation message to a run's TASKRUN.md file.
|
|
177
|
-
*/
|
|
178
139
|
export function appendRunMessage(
|
|
179
140
|
taskDir: string,
|
|
180
141
|
runId: string,
|
|
@@ -189,10 +150,6 @@ export function appendRunMessage(
|
|
|
189
150
|
fs.appendFileSync(path.join(taskDir, runId, "TASKRUN.md"), entry, "utf-8");
|
|
190
151
|
}
|
|
191
152
|
|
|
192
|
-
/**
|
|
193
|
-
* Begin a streaming assistant message — writes the delimiter only.
|
|
194
|
-
* Returns a writer that appends content chunks and finalizes the message.
|
|
195
|
-
*/
|
|
196
153
|
export function beginStreamingMessage(
|
|
197
154
|
taskDir: string,
|
|
198
155
|
runId: string,
|
|
@@ -217,7 +174,7 @@ export class StreamingMessageWriter {
|
|
|
217
174
|
fs.appendFileSync(this.filePath, "\n\n", "utf-8");
|
|
218
175
|
if (attachments?.length) {
|
|
219
176
|
const raw = fs.readFileSync(this.filePath, "utf-8");
|
|
220
|
-
//
|
|
177
|
+
// spliceUserMessage may have created a newer assistant delimiter.
|
|
221
178
|
const pattern = /<!-- palmier:message role="assistant" time="\d+" -->/g;
|
|
222
179
|
let lastMatch: RegExpExecArray | null = null;
|
|
223
180
|
let m;
|
|
@@ -233,12 +190,10 @@ export class StreamingMessageWriter {
|
|
|
233
190
|
}
|
|
234
191
|
|
|
235
192
|
/**
|
|
236
|
-
* Splice a user message into a running assistant stream
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
* write() is just appendFileSync, so subsequent chunks land in
|
|
241
|
-
* the new assistant block.
|
|
193
|
+
* Splice a user message into a running assistant stream: close the current
|
|
194
|
+
* assistant block, write the user message, open a new assistant block. Direct
|
|
195
|
+
* appends only, so an existing StreamingMessageWriter keeps working — its
|
|
196
|
+
* subsequent chunks land in the new block.
|
|
242
197
|
*/
|
|
243
198
|
export function spliceUserMessage(
|
|
244
199
|
taskDir: string,
|
|
@@ -248,22 +203,15 @@ export function spliceUserMessage(
|
|
|
248
203
|
assistantAppend?: string,
|
|
249
204
|
): void {
|
|
250
205
|
const filePath = path.join(taskDir, runId, "TASKRUN.md");
|
|
251
|
-
// 1. Optionally append to the current assistant block (e.g. the input questions)
|
|
252
206
|
if (assistantAppend) {
|
|
253
207
|
fs.appendFileSync(filePath, assistantAppend, "utf-8");
|
|
254
208
|
}
|
|
255
|
-
// 2. End the current assistant block
|
|
256
209
|
fs.appendFileSync(filePath, "\n\n", "utf-8");
|
|
257
|
-
// 3. Write the user message
|
|
258
210
|
appendRunMessage(taskDir, runId, userMsg);
|
|
259
|
-
// 4. Open a new assistant block for subsequent agent output
|
|
260
211
|
const delimiter = `<!-- palmier:message role="assistant" time="${Date.now()}" -->`;
|
|
261
212
|
fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
|
|
262
213
|
}
|
|
263
214
|
|
|
264
|
-
/**
|
|
265
|
-
* Read conversation messages from a run's TASKRUN.md file.
|
|
266
|
-
*/
|
|
267
215
|
export function readRunMessages(taskDir: string, runId: string): ConversationMessage[] {
|
|
268
216
|
const raw = fs.readFileSync(path.join(taskDir, runId, "TASKRUN.md"), "utf-8");
|
|
269
217
|
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
@@ -298,18 +246,11 @@ export function readRunMessages(taskDir: string, runId: string): ConversationMes
|
|
|
298
246
|
return messages;
|
|
299
247
|
}
|
|
300
248
|
|
|
301
|
-
/**
|
|
302
|
-
* Append a history entry to the project-level history.jsonl file.
|
|
303
|
-
*/
|
|
304
249
|
export function appendHistory(projectRoot: string, entry: HistoryEntry): void {
|
|
305
250
|
const historyPath = path.join(projectRoot, "history.jsonl");
|
|
306
251
|
fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
307
252
|
}
|
|
308
253
|
|
|
309
|
-
/**
|
|
310
|
-
* Delete a history entry and its associated run directory.
|
|
311
|
-
* Returns true if the entry was found and removed.
|
|
312
|
-
*/
|
|
313
254
|
export function deleteHistoryEntry(
|
|
314
255
|
projectRoot: string,
|
|
315
256
|
taskId: string,
|
|
@@ -337,7 +278,6 @@ export function deleteHistoryEntry(
|
|
|
337
278
|
|
|
338
279
|
fs.writeFileSync(historyPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
|
|
339
280
|
|
|
340
|
-
// Delete the run directory
|
|
341
281
|
const runDir = path.join(projectRoot, "tasks", taskId, runId);
|
|
342
282
|
if (fs.existsSync(runDir)) {
|
|
343
283
|
fs.rmSync(runDir, { recursive: true, force: true });
|
|
@@ -346,10 +286,7 @@ export function deleteHistoryEntry(
|
|
|
346
286
|
return true;
|
|
347
287
|
}
|
|
348
288
|
|
|
349
|
-
/**
|
|
350
|
-
* Read history entries from history.jsonl with pagination.
|
|
351
|
-
* Returns entries sorted most-recent-first.
|
|
352
|
-
*/
|
|
289
|
+
/** Returns entries most-recent-first. */
|
|
353
290
|
export function readHistory(
|
|
354
291
|
projectRoot: string,
|
|
355
292
|
opts: { offset?: number; limit?: number; task_id?: string },
|
|
@@ -11,8 +11,6 @@ import { handleMcpRequest, getAgentName, getResourceSubscriptions } from "../mcp
|
|
|
11
11
|
import { getTaskDir } from "../task.js";
|
|
12
12
|
import { popEvent } from "../event-queues.js";
|
|
13
13
|
|
|
14
|
-
// ── Bundled PWA asset serving ───────────────────────────────────────────
|
|
15
|
-
|
|
16
14
|
interface CachedAsset {
|
|
17
15
|
data: Buffer;
|
|
18
16
|
contentType: string;
|
|
@@ -41,22 +39,18 @@ function guessContentType(urlPath: string): string {
|
|
|
41
39
|
return CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
/**
|
|
45
|
-
* Read a PWA asset from the bundled pwa/ directory, caching in memory.
|
|
46
|
-
* Returns null if the file does not exist.
|
|
47
|
-
*/
|
|
48
42
|
function getAsset(urlPath: string): CachedAsset | null {
|
|
49
43
|
const cached = assetCache.get(urlPath);
|
|
50
44
|
if (cached) return cached;
|
|
51
45
|
|
|
52
46
|
const filePath = path.join(PWA_DIR, urlPath === "/" ? "index.html" : urlPath);
|
|
53
47
|
|
|
54
|
-
// Prevent path traversal
|
|
48
|
+
// Prevent path traversal.
|
|
55
49
|
if (!filePath.startsWith(PWA_DIR)) return null;
|
|
56
50
|
|
|
57
51
|
try {
|
|
58
52
|
let data = fs.readFileSync(filePath);
|
|
59
|
-
//
|
|
53
|
+
// Marker lets the PWA detect it's served by palmier.
|
|
60
54
|
if (urlPath === "/") {
|
|
61
55
|
const html = data.toString("utf-8").replace("</head>", "<script>window.__PALMIER_SERVE__=true</script></head>");
|
|
62
56
|
data = Buffer.from(html, "utf-8");
|
|
@@ -90,10 +84,6 @@ export function detectLanIp(): string {
|
|
|
90
84
|
return "127.0.0.1";
|
|
91
85
|
}
|
|
92
86
|
|
|
93
|
-
/**
|
|
94
|
-
* Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
|
|
95
|
-
* localhost-only agent endpoints (notify, request-input, confirmation, permission).
|
|
96
|
-
*/
|
|
97
87
|
export async function startHttpTransport(
|
|
98
88
|
config: HostConfig,
|
|
99
89
|
handleRpc: (req: RpcMessage) => Promise<unknown>,
|
|
@@ -126,7 +116,6 @@ export async function startHttpTransport(
|
|
|
126
116
|
resource.subscribe(() => broadcastResourceUpdated(resource.uri));
|
|
127
117
|
}
|
|
128
118
|
|
|
129
|
-
// If a pairing code is provided, pre-register it
|
|
130
119
|
if (pairingCode) {
|
|
131
120
|
const EXPIRY_MS = 24 * 60 * 60 * 1000;
|
|
132
121
|
const timer = setTimeout(() => { pendingPairs.delete(pairingCode); }, EXPIRY_MS);
|
|
@@ -171,9 +160,6 @@ export async function startHttpTransport(
|
|
|
171
160
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
172
161
|
}
|
|
173
162
|
|
|
174
|
-
/**
|
|
175
|
-
* Publish an event via NATS and SSE.
|
|
176
|
-
*/
|
|
177
163
|
async function publishEvent(taskId: string, payload: Record<string, unknown>): Promise<void> {
|
|
178
164
|
const sc = StringCodec();
|
|
179
165
|
const subject = `host-event.${config.hostId}.${taskId}`;
|
|
@@ -191,8 +177,6 @@ export async function startHttpTransport(
|
|
|
191
177
|
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
192
178
|
const pathname = url.pathname;
|
|
193
179
|
|
|
194
|
-
// ── MCP streamable HTTP endpoint ──────────────────────────────────
|
|
195
|
-
|
|
196
180
|
if (req.method === "POST" && pathname === "/mcp") {
|
|
197
181
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
198
182
|
try {
|
|
@@ -204,7 +188,7 @@ export async function startHttpTransport(
|
|
|
204
188
|
res.setHeader("Mcp-Session-Id", result.sessionId);
|
|
205
189
|
}
|
|
206
190
|
if (result.stream && sessionId) {
|
|
207
|
-
// Keep response open as SSE
|
|
191
|
+
// Keep the response open as SSE for server-initiated notifications.
|
|
208
192
|
res.writeHead(200, {
|
|
209
193
|
"Content-Type": "text/event-stream",
|
|
210
194
|
"Cache-Control": "no-cache",
|
|
@@ -227,8 +211,6 @@ export async function startHttpTransport(
|
|
|
227
211
|
return;
|
|
228
212
|
}
|
|
229
213
|
|
|
230
|
-
// ── Auto-generated REST endpoints from MCP tool registry ──────────
|
|
231
|
-
|
|
232
214
|
if (req.method === "POST" && agentToolMap.has(pathname.slice(1))) {
|
|
233
215
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
234
216
|
const tool = agentToolMap.get(pathname.slice(1))!;
|
|
@@ -258,8 +240,6 @@ export async function startHttpTransport(
|
|
|
258
240
|
return;
|
|
259
241
|
}
|
|
260
242
|
|
|
261
|
-
// ── Auto-generated REST endpoints from MCP resource registry ────
|
|
262
|
-
|
|
263
243
|
const matchedResource = req.method === "GET" && agentResources.find((r) => r.restPath === pathname);
|
|
264
244
|
if (matchedResource) {
|
|
265
245
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
@@ -280,8 +260,6 @@ export async function startHttpTransport(
|
|
|
280
260
|
return;
|
|
281
261
|
}
|
|
282
262
|
|
|
283
|
-
// ── Event queue pop (used by event-triggered palmier run) ─────────
|
|
284
|
-
|
|
285
263
|
if (req.method === "POST" && pathname === "/task-event/pop") {
|
|
286
264
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
287
265
|
const taskId = url.searchParams.get("taskId");
|
|
@@ -293,8 +271,6 @@ export async function startHttpTransport(
|
|
|
293
271
|
return;
|
|
294
272
|
}
|
|
295
273
|
|
|
296
|
-
// ── Localhost-only endpoints (no auth) ─────────────────────────────
|
|
297
|
-
|
|
298
274
|
if (req.method === "POST" && pathname === "/event") {
|
|
299
275
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
300
276
|
try {
|
|
@@ -334,8 +310,6 @@ export async function startHttpTransport(
|
|
|
334
310
|
return;
|
|
335
311
|
}
|
|
336
312
|
|
|
337
|
-
// ── POST /request-permission — held connection ──────────────────────
|
|
338
|
-
|
|
339
313
|
if (req.method === "POST" && pathname === "/request-permission") {
|
|
340
314
|
if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
|
|
341
315
|
try {
|
|
@@ -376,8 +350,6 @@ export async function startHttpTransport(
|
|
|
376
350
|
return;
|
|
377
351
|
}
|
|
378
352
|
|
|
379
|
-
// ── Public pair endpoint — no auth, PWA posts pairing code here ────────
|
|
380
|
-
|
|
381
353
|
if (req.method === "POST" && pathname === "/pair") {
|
|
382
354
|
try {
|
|
383
355
|
const body = await readBody(req);
|
|
@@ -404,16 +376,14 @@ export async function startHttpTransport(
|
|
|
404
376
|
return;
|
|
405
377
|
}
|
|
406
378
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
// Skip service worker and manifest — they require HTTPS which LAN mode doesn't use
|
|
379
|
+
// Service worker and manifest require HTTPS, which LAN mode doesn't use.
|
|
410
380
|
const SKIP = new Set(["/registerSW.js", "/service-worker.js", "/manifest.webmanifest"]);
|
|
411
381
|
|
|
412
382
|
const isApiRoute = pathname === "/events" || pathname.startsWith("/rpc/");
|
|
413
383
|
if (!isApiRoute) {
|
|
414
384
|
if (SKIP.has(pathname)) { sendJson(res, 404, { error: "Not found" }); return; }
|
|
415
385
|
|
|
416
|
-
//
|
|
386
|
+
// Fall back to index.html for SPA routing.
|
|
417
387
|
let asset = getAsset(pathname);
|
|
418
388
|
if (!asset && pathname !== "/") {
|
|
419
389
|
asset = getAsset("/");
|
|
@@ -428,14 +398,12 @@ export async function startHttpTransport(
|
|
|
428
398
|
return;
|
|
429
399
|
}
|
|
430
400
|
|
|
431
|
-
//
|
|
432
|
-
|
|
401
|
+
// Localhost is trusted; all other API callers require a client token.
|
|
433
402
|
if (!isLocalhost(req) && !checkAuth(req)) {
|
|
434
403
|
sendJson(res, 401, { error: "Unauthorized" });
|
|
435
404
|
return;
|
|
436
405
|
}
|
|
437
406
|
|
|
438
|
-
// SSE event stream
|
|
439
407
|
if (req.method === "GET" && pathname === "/events") {
|
|
440
408
|
res.writeHead(200, {
|
|
441
409
|
"Content-Type": "text/event-stream",
|
|
@@ -456,7 +424,6 @@ export async function startHttpTransport(
|
|
|
456
424
|
return;
|
|
457
425
|
}
|
|
458
426
|
|
|
459
|
-
// RPC endpoint: POST /rpc/<method>
|
|
460
427
|
if (req.method === "POST" && pathname.startsWith("/rpc/")) {
|
|
461
428
|
const method = pathname.slice("/rpc/".length);
|
|
462
429
|
if (!method) { sendJson(res, 400, { error: "Missing RPC method" }); return; }
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { StringCodec, type NatsConnection, type Msg, type Subscription } from "nats";
|
|
2
2
|
import type { HostConfig, RpcMessage } from "../types.js";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Start the NATS transport using an existing connection.
|
|
6
|
-
* Subscribe to RPC subjects and dispatch to handler.
|
|
7
|
-
*/
|
|
8
4
|
export async function startNatsTransport(
|
|
9
5
|
config: HostConfig,
|
|
10
6
|
handleRpc: (req: RpcMessage) => Promise<unknown>,
|
|
@@ -16,7 +12,6 @@ export async function startNatsTransport(
|
|
|
16
12
|
console.log(`[nats] Subscribing to: ${subject}`);
|
|
17
13
|
const sub = nc.subscribe(subject);
|
|
18
14
|
|
|
19
|
-
// Graceful shutdown
|
|
20
15
|
const shutdown = async () => {
|
|
21
16
|
console.log("[nats] Shutting down...");
|
|
22
17
|
sub.unsubscribe();
|
|
@@ -28,12 +23,11 @@ export async function startNatsTransport(
|
|
|
28
23
|
process.on("SIGTERM", shutdown);
|
|
29
24
|
|
|
30
25
|
async function processMessage(msg: Msg) {
|
|
31
|
-
//
|
|
26
|
+
// Subject format: ...rpc.<method parts>
|
|
32
27
|
const subjectTokens = msg.subject.split(".");
|
|
33
28
|
const rpcIdx = subjectTokens.indexOf("rpc");
|
|
34
29
|
const method = rpcIdx >= 0 ? subjectTokens.slice(rpcIdx + 1).join(".") : "";
|
|
35
30
|
|
|
36
|
-
// Parse params from message body
|
|
37
31
|
let params: Record<string, unknown> = {};
|
|
38
32
|
if (msg.data && msg.data.length > 0) {
|
|
39
33
|
const raw = sc.decode(msg.data).trim();
|
|
@@ -50,7 +44,7 @@ export async function startNatsTransport(
|
|
|
50
44
|
}
|
|
51
45
|
}
|
|
52
46
|
|
|
53
|
-
//
|
|
47
|
+
// PWA includes the client token in the payload.
|
|
54
48
|
const clientToken = typeof params.clientToken === "string" ? params.clientToken : undefined;
|
|
55
49
|
delete params.clientToken;
|
|
56
50
|
|
|
@@ -72,7 +66,7 @@ export async function startNatsTransport(
|
|
|
72
66
|
|
|
73
67
|
async function consumeSubscription(subscription: Subscription) {
|
|
74
68
|
for await (const msg of subscription) {
|
|
75
|
-
//
|
|
69
|
+
// Don't await — heartbeats must keep flowing while RPC runs.
|
|
76
70
|
processMessage(msg);
|
|
77
71
|
}
|
|
78
72
|
}
|
package/src/types.ts
CHANGED
|
@@ -7,12 +7,10 @@ export interface HostConfig {
|
|
|
7
7
|
natsJwt?: string;
|
|
8
8
|
natsNkeySeed?: string;
|
|
9
9
|
|
|
10
|
-
// Detected agent CLIs
|
|
11
10
|
agents?: Array<{ key: string; label: string; supportsPermissions: boolean; supportsYolo: boolean }>;
|
|
12
11
|
|
|
13
|
-
// HTTP server port (default 7256)
|
|
14
12
|
httpPort?: number;
|
|
15
|
-
|
|
13
|
+
/** Whether to accept non-localhost HTTP connections. */
|
|
16
14
|
lanEnabled?: boolean;
|
|
17
15
|
}
|
|
18
16
|
|
|
@@ -25,8 +23,8 @@ export interface TaskFrontmatter {
|
|
|
25
23
|
* Task schedule.
|
|
26
24
|
* - `crons`: `schedule_values` holds cron expressions (e.g. "0 9 * * *")
|
|
27
25
|
* - `specific_times`: `schedule_values` holds local datetime strings (e.g. "2026-04-20T09:00")
|
|
28
|
-
* - `on_new_notification`: fires on each new Android notification from NATS
|
|
29
|
-
* - `on_new_sms`: fires on each new SMS from NATS
|
|
26
|
+
* - `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.
|
|
27
|
+
* - `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.
|
|
30
28
|
*/
|
|
31
29
|
schedule_type?: "crons" | "specific_times" | "on_new_notification" | "on_new_sms";
|
|
32
30
|
schedule_values?: string[];
|
|
@@ -50,11 +48,6 @@ export interface ParsedTask {
|
|
|
50
48
|
*/
|
|
51
49
|
export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
|
|
52
50
|
|
|
53
|
-
/**
|
|
54
|
-
* Persisted to `status.json` in the task directory. Used for crash detection
|
|
55
|
-
* (checkStaleTasks) and abort signalling. Interactive request flows (confirmation,
|
|
56
|
-
* permission, input) are handled via held HTTP connections on the serve daemon.
|
|
57
|
-
*/
|
|
58
51
|
export interface TaskStatus {
|
|
59
52
|
running_state: TaskRunningState;
|
|
60
53
|
time_stamp: number;
|
package/src/update-checker.ts
CHANGED
|
@@ -12,10 +12,7 @@ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "
|
|
|
12
12
|
export const isDevBuild = fs.existsSync(path.join(packageRoot, ".git"));
|
|
13
13
|
export const currentVersion = isDevBuild ? `${pkg.version}-dev` : pkg.version;
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* Run the update and restart the daemon.
|
|
17
|
-
* Returns an error message if the update fails.
|
|
18
|
-
*/
|
|
15
|
+
/** Returns an error message if the update fails. */
|
|
19
16
|
export async function performUpdate(): Promise<string | null> {
|
|
20
17
|
try {
|
|
21
18
|
const { output, exitCode } = await spawnCommand("npm", ["update", "-g", "palmier"], {
|
|
@@ -28,7 +25,7 @@ export async function performUpdate(): Promise<string | null> {
|
|
|
28
25
|
return `Update failed. Please run manually:\nnpm update -g palmier`;
|
|
29
26
|
}
|
|
30
27
|
console.log("[update] Update installed, restarting daemon...");
|
|
31
|
-
//
|
|
28
|
+
// Delay so the RPC response finishes sending first.
|
|
32
29
|
setTimeout(() => {
|
|
33
30
|
getPlatform().restartDaemon().catch((err) => {
|
|
34
31
|
console.error("[update] Restart failed:", err);
|
|
@@ -35,16 +35,15 @@ requires_confirmation: false
|
|
|
35
35
|
assert.equal(result.frontmatter.agent, "claude");
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
it("defaults
|
|
38
|
+
it("defaults schedule_enabled to true", () => {
|
|
39
39
|
const content = `---
|
|
40
40
|
id: abc123
|
|
41
41
|
user_prompt: Do something
|
|
42
|
-
triggers: []
|
|
43
42
|
requires_confirmation: false
|
|
44
43
|
---`;
|
|
45
44
|
|
|
46
45
|
const result = parseTaskContent(content);
|
|
47
|
-
assert.equal(result.frontmatter.
|
|
46
|
+
assert.equal(result.frontmatter.schedule_enabled, true);
|
|
48
47
|
});
|
|
49
48
|
|
|
50
49
|
it("derives name from user_prompt when not specified", () => {
|
package/test/windows-xml.test.ts
CHANGED
|
@@ -1,55 +1,54 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
3
|
+
import { scheduleValueToXml, buildTaskXml } from "../src/platform/windows.js";
|
|
4
4
|
|
|
5
|
-
describe("
|
|
6
|
-
it("converts a
|
|
7
|
-
const xml =
|
|
5
|
+
describe("scheduleValueToXml", () => {
|
|
6
|
+
it("converts a specific_times value to TimeTrigger", () => {
|
|
7
|
+
const xml = scheduleValueToXml("specific_times", "2026-03-28T09:00");
|
|
8
8
|
assert.equal(xml, "<TimeTrigger><StartBoundary>2026-03-28T09:00:00</StartBoundary></TimeTrigger>");
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it("converts hourly cron to TimeTrigger with PT1H repetition", () => {
|
|
12
|
-
const xml =
|
|
12
|
+
const xml = scheduleValueToXml("crons", "0 * * * *");
|
|
13
13
|
assert.ok(xml.includes("<Interval>PT1H</Interval>"), "should have hourly interval");
|
|
14
14
|
assert.ok(xml.includes("<TimeTrigger>"), "should be a TimeTrigger");
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it("converts daily cron to CalendarTrigger with DaysInterval", () => {
|
|
18
|
-
const xml =
|
|
18
|
+
const xml = scheduleValueToXml("crons", "30 9 * * *");
|
|
19
19
|
assert.ok(xml.includes("<ScheduleByDay>"), "should use ScheduleByDay");
|
|
20
20
|
assert.ok(xml.includes("<DaysInterval>1</DaysInterval>"), "should have interval 1");
|
|
21
21
|
assert.ok(xml.includes("T09:30:00"), "should encode time as 09:30");
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it("converts weekly cron to CalendarTrigger with DaysOfWeek", () => {
|
|
25
|
-
const xml =
|
|
25
|
+
const xml = scheduleValueToXml("crons", "0 10 * * 1");
|
|
26
26
|
assert.ok(xml.includes("<ScheduleByWeek>"), "should use ScheduleByWeek");
|
|
27
27
|
assert.ok(xml.includes("<Monday />"), "day 1 should be Monday");
|
|
28
28
|
assert.ok(xml.includes("T10:00:00"), "should encode time as 10:00");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
it("converts weekly cron for Sunday (day 0)", () => {
|
|
32
|
-
const xml =
|
|
32
|
+
const xml = scheduleValueToXml("crons", "0 8 * * 0");
|
|
33
33
|
assert.ok(xml.includes("<Sunday />"), "day 0 should be Sunday");
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it("converts weekly cron for Sunday (day 7)", () => {
|
|
37
|
-
const xml =
|
|
37
|
+
const xml = scheduleValueToXml("crons", "0 8 * * 7");
|
|
38
38
|
assert.ok(xml.includes("<Sunday />"), "day 7 should also be Sunday");
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it("converts monthly cron to CalendarTrigger with DaysOfMonth", () => {
|
|
42
|
-
const xml =
|
|
42
|
+
const xml = scheduleValueToXml("crons", "0 14 15 * *");
|
|
43
43
|
assert.ok(xml.includes("<ScheduleByMonth>"), "should use ScheduleByMonth");
|
|
44
44
|
assert.ok(xml.includes("<Day>15</Day>"), "should have day 15");
|
|
45
45
|
assert.ok(xml.includes("T14:00:00"), "should encode time as 14:00");
|
|
46
|
-
// All months should be listed
|
|
47
46
|
assert.ok(xml.includes("<January />"), "should include January");
|
|
48
47
|
assert.ok(xml.includes("<December />"), "should include December");
|
|
49
48
|
});
|
|
50
49
|
|
|
51
50
|
it("throws on invalid cron expression", () => {
|
|
52
|
-
assert.throws(() =>
|
|
51
|
+
assert.throws(() => scheduleValueToXml("crons", "bad"), /Invalid cron/);
|
|
53
52
|
});
|
|
54
53
|
});
|
|
55
54
|
|