palmier 0.8.1 → 0.8.4
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 +16 -14
- 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/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 +3 -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 +29 -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 +8 -7
- package/dist/platform/linux.d.ts +3 -9
- package/dist/platform/linux.js +9 -20
- package/dist/platform/macos.d.ts +32 -0
- package/dist/platform/macos.js +287 -0
- 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-499vYQvR.js +120 -0
- package/dist/pwa/assets/{index-CQxcuDhM.css → index-UaZFu6XL.css} +1 -1
- package/dist/pwa/assets/{web-DOyOiwsW.js → web-Bp48ONY3.js} +1 -1
- package/dist/pwa/assets/{web-D7Kq3Nvk.js → web-CyJutAy4.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 +14 -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 +7 -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 +325 -22
- package/palmier-server/pwa/src/App.tsx +2 -0
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +288 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +20 -207
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
- package/palmier-server/pwa/src/components/SessionComposer.tsx +11 -2
- package/palmier-server/pwa/src/components/SessionsView.tsx +60 -32
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
- package/palmier-server/pwa/src/components/TaskCard.tsx +1 -1
- package/palmier-server/pwa/src/components/TaskForm.tsx +207 -5
- package/palmier-server/pwa/src/components/TasksView.tsx +3 -1
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +18 -2
- package/palmier-server/pwa/src/pages/Dashboard.tsx +13 -6
- package/palmier-server/pwa/src/pages/PairHost.tsx +3 -1
- package/palmier-server/pwa/src/pages/PairSetup.tsx +70 -0
- 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/commands/info.ts +0 -5
- package/src/commands/init.ts +2 -11
- package/src/commands/pair.ts +3 -12
- package/src/commands/restart.ts +0 -3
- package/src/commands/run.ts +18 -65
- package/src/commands/serve.ts +28 -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 +5 -7
- package/src/platform/linux.ts +9 -20
- package/src/platform/macos.ts +310 -0
- package/src/platform/platform.ts +1 -4
- package/src/platform/windows.ts +19 -40
- package/src/rpc-handler.ts +14 -47
- package/src/spawn-command.ts +11 -27
- package/src/task.ts +7 -70
- package/src/transports/http-transport.ts +7 -39
- package/src/transports/nats-transport.ts +3 -9
- package/src/types.ts +3 -10
- package/src/update-checker.ts +2 -5
- package/test/macos-plist.test.ts +112 -0
- 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/dist/commands/run.js
CHANGED
|
@@ -10,16 +10,13 @@ import { getPlatform } from "../platform/index.js";
|
|
|
10
10
|
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
|
|
11
11
|
import { publishHostEvent } from "../events.js";
|
|
12
12
|
/**
|
|
13
|
-
* Invoke the agent CLI
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* The `invokeTask` is the ParsedTask whose prompt is passed to the agent
|
|
17
|
-
* (for command-triggered mode this is the per-line augmented task).
|
|
13
|
+
* Invoke the agent CLI in a continuation loop to handle permission requests.
|
|
14
|
+
* `invokeTask` is the ParsedTask whose prompt is passed to the agent (in
|
|
15
|
+
* command-triggered mode this is the per-line augmented task).
|
|
18
16
|
*/
|
|
19
17
|
async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
20
18
|
// eslint-disable-next-line no-constant-condition
|
|
21
19
|
while (true) {
|
|
22
|
-
// Stream agent output to TASKRUN.md in real-time, throttled to 500ms
|
|
23
20
|
const writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now());
|
|
24
21
|
let lineBuf = "";
|
|
25
22
|
let notifyPending = false;
|
|
@@ -56,11 +53,9 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
56
53
|
const outcome = result.exitCode !== 0 ? "failed" : parseTaskOutcome(result.output);
|
|
57
54
|
const reportFiles = parseReportFiles(result.output);
|
|
58
55
|
const requiredPermissions = parsePermissions(result.output);
|
|
59
|
-
// Flush remaining buffered content
|
|
60
56
|
if (lineBuf && !lineBuf.startsWith("[PALMIER")) {
|
|
61
57
|
writer.write(lineBuf);
|
|
62
58
|
}
|
|
63
|
-
// Include permission requests in the assistant message
|
|
64
59
|
if (requiredPermissions.length > 0) {
|
|
65
60
|
const permLines = requiredPermissions.map((p) => `- **${p.name}** ${p.description}`).join("\n");
|
|
66
61
|
writer.write(`\n\n**Permissions requested:**\n${permLines}\n`);
|
|
@@ -75,7 +70,6 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
75
70
|
report_files: reportFiles,
|
|
76
71
|
});
|
|
77
72
|
}
|
|
78
|
-
// Permission handling — agent requested permissions
|
|
79
73
|
if (requiredPermissions.length > 0) {
|
|
80
74
|
const response = await requestPermission(ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
|
|
81
75
|
if (response === "aborted") {
|
|
@@ -103,31 +97,22 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
103
97
|
else {
|
|
104
98
|
ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
|
|
105
99
|
}
|
|
106
|
-
//
|
|
100
|
+
// Retry with the new permissions if the agent failed.
|
|
107
101
|
if (outcome === "failed") {
|
|
108
102
|
continue;
|
|
109
103
|
}
|
|
110
104
|
}
|
|
111
|
-
// Normal completion (success or terminal failure)
|
|
112
105
|
return { outcome };
|
|
113
106
|
}
|
|
114
107
|
}
|
|
115
|
-
/**
|
|
116
|
-
* Strip [PALMIER_*] marker lines from agent output.
|
|
117
|
-
*/
|
|
118
108
|
export function stripPalmierMarkers(output) {
|
|
119
109
|
return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
|
|
120
110
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Append a conversation message to the RESULT file and notify connected clients.
|
|
123
|
-
*/
|
|
124
111
|
async function appendAndNotify(ctx, msg) {
|
|
125
112
|
appendRunMessage(ctx.taskDir, ctx.runId, msg);
|
|
126
113
|
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
127
114
|
}
|
|
128
|
-
/**
|
|
129
|
-
* Find the latest run dir that has no status messages yet (just created by the RPC handler).
|
|
130
|
-
*/
|
|
115
|
+
/** The latest run dir with no status messages yet — freshly created by the RPC handler. */
|
|
131
116
|
function findLatestPendingRunId(taskDir) {
|
|
132
117
|
const dirs = fs.readdirSync(taskDir)
|
|
133
118
|
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
|
|
@@ -140,8 +125,8 @@ function findLatestPendingRunId(taskDir) {
|
|
|
140
125
|
return hasStatus ? null : latest;
|
|
141
126
|
}
|
|
142
127
|
/**
|
|
143
|
-
* If the RPC handler already wrote "aborted"
|
|
144
|
-
*
|
|
128
|
+
* If the RPC handler already wrote "aborted" (via task.abort), respect that
|
|
129
|
+
* instead of overwriting with the process's own outcome.
|
|
145
130
|
*/
|
|
146
131
|
function resolveOutcome(taskDir, outcome) {
|
|
147
132
|
const current = readTaskStatus(taskDir);
|
|
@@ -149,9 +134,6 @@ function resolveOutcome(taskDir, outcome) {
|
|
|
149
134
|
return "aborted";
|
|
150
135
|
return outcome;
|
|
151
136
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Execute a task by ID.
|
|
154
|
-
*/
|
|
155
137
|
export async function runCommand(taskId) {
|
|
156
138
|
const config = loadConfig();
|
|
157
139
|
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
@@ -159,7 +141,6 @@ export async function runCommand(taskId) {
|
|
|
159
141
|
console.log(`Running task: ${taskId}`);
|
|
160
142
|
let nc;
|
|
161
143
|
const taskName = task.frontmatter.name;
|
|
162
|
-
// Use existing run dir if just created by RPC, otherwise create a new one
|
|
163
144
|
const existingRunId = findLatestPendingRunId(taskDir);
|
|
164
145
|
const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now(), task.frontmatter.agent);
|
|
165
146
|
if (!existingRunId) {
|
|
@@ -175,7 +156,6 @@ export async function runCommand(taskId) {
|
|
|
175
156
|
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, runId);
|
|
176
157
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
177
158
|
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
178
|
-
// If requires_confirmation, notify clients and wait
|
|
179
159
|
if (task.frontmatter.requires_confirmation) {
|
|
180
160
|
const confirmed = await requestConfirmation(config, task, taskDir);
|
|
181
161
|
const confirmPrompt = `**Task Confirmation**\n\nRun task "${taskName || task.frontmatter.user_prompt}"?`;
|
|
@@ -194,7 +174,6 @@ export async function runCommand(taskId) {
|
|
|
194
174
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "confirmation" });
|
|
195
175
|
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
196
176
|
}
|
|
197
|
-
// Shared invocation context
|
|
198
177
|
const guiEnv = getPlatform().getGuiEnv();
|
|
199
178
|
const agent = getAgent(task.frontmatter.agent);
|
|
200
179
|
const ctx = {
|
|
@@ -202,7 +181,6 @@ export async function runCommand(taskId) {
|
|
|
202
181
|
transientPermissions: [],
|
|
203
182
|
};
|
|
204
183
|
if (task.frontmatter.command) {
|
|
205
|
-
// Command-triggered mode
|
|
206
184
|
const result = await runCommandTriggeredMode(ctx);
|
|
207
185
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
208
186
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
@@ -211,7 +189,6 @@ export async function runCommand(taskId) {
|
|
|
211
189
|
}
|
|
212
190
|
else if (task.frontmatter.schedule_type === "on_new_notification"
|
|
213
191
|
|| task.frontmatter.schedule_type === "on_new_sms") {
|
|
214
|
-
// Event-triggered mode (driven by NATS pub/sub of device notifications/SMS)
|
|
215
192
|
const result = await runEventTriggeredMode(ctx);
|
|
216
193
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
217
194
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
@@ -219,7 +196,6 @@ export async function runCommand(taskId) {
|
|
|
219
196
|
console.log(`Task ${taskId} completed (event-triggered).`);
|
|
220
197
|
}
|
|
221
198
|
else {
|
|
222
|
-
// Standard execution — add user prompt as first message
|
|
223
199
|
await appendAndNotify(ctx, {
|
|
224
200
|
role: "user",
|
|
225
201
|
time: Date.now(),
|
|
@@ -254,11 +230,9 @@ const MAX_LOG_ENTRIES = 1000;
|
|
|
254
230
|
/** Max input line length (chars). Long emails can take up to 200k chars. */
|
|
255
231
|
const MAX_LINE_LENGTH = 200_000;
|
|
256
232
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* invokes the agent CLI with the user's prompt augmented by that line.
|
|
261
|
-
* Processes lines sequentially with a bounded queue.
|
|
233
|
+
* Spawn a long-running shell command and invoke the agent CLI once per stdout
|
|
234
|
+
* line, with the user's prompt augmented by that line. Sequential with a
|
|
235
|
+
* bounded queue.
|
|
262
236
|
*/
|
|
263
237
|
async function runCommandTriggeredMode(ctx) {
|
|
264
238
|
const commandStr = ctx.task.frontmatter.command;
|
|
@@ -280,7 +254,6 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
280
254
|
function appendLog(line, agentOutput, outcome) {
|
|
281
255
|
const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
|
|
282
256
|
fs.appendFileSync(logPath, entry, "utf-8");
|
|
283
|
-
// Trim log if too large (keep last MAX_LOG_ENTRIES entries)
|
|
284
257
|
try {
|
|
285
258
|
const content = fs.readFileSync(logPath, "utf-8");
|
|
286
259
|
const entries = content.split("\n---\n").filter(Boolean);
|
|
@@ -312,7 +285,7 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
312
285
|
invocationsFailed++;
|
|
313
286
|
}
|
|
314
287
|
appendLog(line, "", result.outcome);
|
|
315
|
-
//
|
|
288
|
+
// Signal "waiting for more input" in the UI.
|
|
316
289
|
appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
|
|
317
290
|
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
318
291
|
}
|
|
@@ -353,7 +326,6 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
353
326
|
stderrBuf += chunk;
|
|
354
327
|
process.stderr.write(d);
|
|
355
328
|
});
|
|
356
|
-
// Wait for command to exit
|
|
357
329
|
const exitCode = await new Promise((resolve) => {
|
|
358
330
|
child.on("close", (code) => {
|
|
359
331
|
commandExited = true;
|
|
@@ -368,7 +340,6 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
368
340
|
resolve(1);
|
|
369
341
|
});
|
|
370
342
|
});
|
|
371
|
-
// Wait for any remaining queued lines to finish processing
|
|
372
343
|
if (lineQueue.length > 0 || processing) {
|
|
373
344
|
await new Promise((resolve) => {
|
|
374
345
|
resolveWhenDone = resolve;
|
|
@@ -390,13 +361,10 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
390
361
|
return { outcome: "finished", endTime };
|
|
391
362
|
}
|
|
392
363
|
/**
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* into the user prompt. The run process itself holds no NATS subscription;
|
|
398
|
-
* the daemon handles that and atomically clears the active flag when we see
|
|
399
|
-
* an empty pop, so it can fire up a fresh run on the next incoming event.
|
|
364
|
+
* Drain the daemon-owned per-task event queue via /task-event/pop, invoking
|
|
365
|
+
* the agent once per event. The run process holds no NATS subscription — the
|
|
366
|
+
* daemon owns that and atomically clears the active flag on empty pop so it
|
|
367
|
+
* can fire a fresh run on the next incoming event.
|
|
400
368
|
*/
|
|
401
369
|
async function runEventTriggeredMode(ctx) {
|
|
402
370
|
const scheduleType = ctx.task.frontmatter.schedule_type;
|
|
@@ -488,10 +456,6 @@ async function requestConfirmation(config, task, taskDir) {
|
|
|
488
456
|
});
|
|
489
457
|
return confirmed;
|
|
490
458
|
}
|
|
491
|
-
/**
|
|
492
|
-
* Extract report file names from agent output.
|
|
493
|
-
* Looks for lines matching: [PALMIER_REPORT] <filename>
|
|
494
|
-
*/
|
|
495
459
|
const ALLOWED_REPORT_EXT = [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
|
|
496
460
|
export function parseReportFiles(output) {
|
|
497
461
|
const regex = new RegExp(`^\\${TASK_REPORT_PREFIX}\\s+(.+)$`, "gm");
|
|
@@ -499,7 +463,7 @@ export function parseReportFiles(output) {
|
|
|
499
463
|
let match;
|
|
500
464
|
while ((match = regex.exec(output)) !== null) {
|
|
501
465
|
const name = match[1].trim();
|
|
502
|
-
// Skip placeholder examples echoed from the prompt (e.g. "<filename>")
|
|
466
|
+
// Skip placeholder examples echoed from the prompt (e.g. "<filename>").
|
|
503
467
|
if (!name || name.startsWith("<"))
|
|
504
468
|
continue;
|
|
505
469
|
const ext = name.lastIndexOf(".") >= 0 ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
|
|
@@ -509,17 +473,13 @@ export function parseReportFiles(output) {
|
|
|
509
473
|
}
|
|
510
474
|
return files;
|
|
511
475
|
}
|
|
512
|
-
/**
|
|
513
|
-
* Extract required permissions from agent output.
|
|
514
|
-
* Looks for lines matching: [PALMIER_PERMISSION] <tool> | <description>
|
|
515
|
-
*/
|
|
516
476
|
export function parsePermissions(output) {
|
|
517
477
|
const regex = new RegExp(`^\\${TASK_PERMISSION_PREFIX}\\s+(.+)$`, "gm");
|
|
518
478
|
const perms = [];
|
|
519
479
|
let match;
|
|
520
480
|
while ((match = regex.exec(output)) !== null) {
|
|
521
481
|
const raw = match[1].trim();
|
|
522
|
-
// Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>")
|
|
482
|
+
// Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>").
|
|
523
483
|
if (raw.startsWith("<"))
|
|
524
484
|
continue;
|
|
525
485
|
const sep = raw.indexOf("|");
|
|
@@ -532,10 +492,7 @@ export function parsePermissions(output) {
|
|
|
532
492
|
}
|
|
533
493
|
return perms;
|
|
534
494
|
}
|
|
535
|
-
/**
|
|
536
|
-
* Parse the agent's output for success/failure markers.
|
|
537
|
-
* Falls back to "finished" if no marker is found.
|
|
538
|
-
*/
|
|
495
|
+
/** Falls back to "finished" if no success/failure marker is found. */
|
|
539
496
|
export function parseTaskOutcome(output) {
|
|
540
497
|
const lastChunk = output.slice(-500);
|
|
541
498
|
const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
|
package/dist/commands/serve.d.ts
CHANGED
package/dist/commands/serve.js
CHANGED
|
@@ -18,11 +18,8 @@ import { enqueueEvent } from "../event-queues.js";
|
|
|
18
18
|
const POLL_INTERVAL_MS = 30_000;
|
|
19
19
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* Since run.ts creates the RESULT file and history entry at start, we just need to
|
|
25
|
-
* finalize the existing RESULT file, append a failed status entry, and broadcast.
|
|
21
|
+
* Reconcile tasks stuck in "started" whose process is no longer alive.
|
|
22
|
+
* The system scheduler (Task Scheduler / systemd) is the authoritative source.
|
|
26
23
|
*/
|
|
27
24
|
async function checkStaleTasks(config, nc) {
|
|
28
25
|
const tasksJsonl = path.join(config.projectRoot, "tasks.jsonl");
|
|
@@ -42,13 +39,11 @@ async function checkStaleTasks(config, nc) {
|
|
|
42
39
|
const status = readTaskStatus(taskDir);
|
|
43
40
|
if (!status || status.running_state !== "started")
|
|
44
41
|
continue;
|
|
45
|
-
// Ask the system scheduler if the task is still running
|
|
46
42
|
if (platform.isTaskRunning(taskId))
|
|
47
43
|
continue;
|
|
48
44
|
console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
|
|
49
45
|
const endTime = Date.now();
|
|
50
46
|
writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
|
|
51
|
-
// Find the latest run directory (created by run.ts at start)
|
|
52
47
|
const runId = fs.readdirSync(taskDir)
|
|
53
48
|
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
|
|
54
49
|
.sort()
|
|
@@ -65,7 +60,7 @@ async function checkStaleTasks(config, nc) {
|
|
|
65
60
|
try {
|
|
66
61
|
taskName = parseTaskFile(taskDir).frontmatter.name || taskId;
|
|
67
62
|
}
|
|
68
|
-
catch { /*
|
|
63
|
+
catch { /* fallback to taskId */ }
|
|
69
64
|
await publishHostEvent(nc, config.hostId, taskId, {
|
|
70
65
|
event_type: "running-state",
|
|
71
66
|
running_state: "failed",
|
|
@@ -73,15 +68,11 @@ async function checkStaleTasks(config, nc) {
|
|
|
73
68
|
});
|
|
74
69
|
}
|
|
75
70
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Start the persistent RPC handler (NATS + HTTP).
|
|
78
|
-
*/
|
|
79
71
|
export async function serveCommand() {
|
|
80
72
|
const config = loadConfig();
|
|
81
|
-
//
|
|
73
|
+
// PID file lets `palmier restart` find us regardless of how we were started
|
|
82
74
|
fs.writeFileSync(DAEMON_PID_FILE, String(process.pid), "utf-8");
|
|
83
75
|
console.log("Starting...");
|
|
84
|
-
// Re-detect agents on every daemon start
|
|
85
76
|
const agents = await detectAgents();
|
|
86
77
|
config.agents = agents;
|
|
87
78
|
saveConfig(config);
|
|
@@ -94,9 +85,8 @@ export async function serveCommand() {
|
|
|
94
85
|
catch (err) {
|
|
95
86
|
console.warn(`[nats] Connection failed (server mode unavailable): ${err}`);
|
|
96
87
|
}
|
|
97
|
-
// Reconcile any tasks stuck from before daemon started
|
|
98
88
|
await checkStaleTasks(config, nc);
|
|
99
|
-
//
|
|
89
|
+
// Reinstall scheduler entries for all tasks (recovery after init/reinstall)
|
|
100
90
|
const platform = getPlatform();
|
|
101
91
|
const allTasks = listTasks(config.projectRoot);
|
|
102
92
|
for (const task of allTasks) {
|
|
@@ -107,7 +97,6 @@ export async function serveCommand() {
|
|
|
107
97
|
console.error(`Warning: failed to install timer for task ${task.frontmatter.id}: ${err}`);
|
|
108
98
|
}
|
|
109
99
|
}
|
|
110
|
-
// Poll for crashed tasks every 30 seconds
|
|
111
100
|
setInterval(() => {
|
|
112
101
|
checkStaleTasks(config, nc).catch((err) => {
|
|
113
102
|
console.error("[monitor] Error checking stale tasks:", err);
|
|
@@ -115,18 +104,30 @@ export async function serveCommand() {
|
|
|
115
104
|
}, POLL_INTERVAL_MS);
|
|
116
105
|
const handleRpc = createRpcHandler(config, nc);
|
|
117
106
|
const httpPort = config.httpPort ?? 7256;
|
|
118
|
-
// Start NATS transport (loops forever, fire-and-forget)
|
|
119
107
|
if (nc) {
|
|
120
108
|
startNatsTransport(config, handleRpc, nc);
|
|
121
|
-
// Subscribe to device notifications and SMS from Android
|
|
122
109
|
const sc = StringCodec();
|
|
123
|
-
//
|
|
124
|
-
function
|
|
110
|
+
// Match phone numbers regardless of formatting; letters preserved for shortcodes.
|
|
111
|
+
function normalizeSender(raw) {
|
|
112
|
+
return raw.replace(/[\s\-()+]/g, "").toLowerCase();
|
|
113
|
+
}
|
|
114
|
+
function dispatchDeviceEvent(scheduleType, payload, parsed) {
|
|
125
115
|
for (const task of listTasks(config.projectRoot)) {
|
|
126
116
|
if (task.frontmatter.schedule_type !== scheduleType)
|
|
127
117
|
continue;
|
|
128
118
|
if (!task.frontmatter.schedule_enabled)
|
|
129
119
|
continue;
|
|
120
|
+
if (scheduleType === "on_new_notification" && task.frontmatter.schedule_values && task.frontmatter.schedule_values.length > 0) {
|
|
121
|
+
const pkg = parsed?.packageName;
|
|
122
|
+
if (!pkg || !task.frontmatter.schedule_values.includes(pkg))
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (scheduleType === "on_new_sms" && task.frontmatter.schedule_values && task.frontmatter.schedule_values.length > 0) {
|
|
126
|
+
const sender = parsed?.sender;
|
|
127
|
+
const normalizedSender = sender ? normalizeSender(sender) : "";
|
|
128
|
+
if (!normalizedSender || !task.frontmatter.schedule_values.some((s) => normalizeSender(s) === normalizedSender))
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
130
131
|
const { shouldStart } = enqueueEvent(task.frontmatter.id, payload);
|
|
131
132
|
if (shouldStart) {
|
|
132
133
|
platform.startTask(task.frontmatter.id).catch((err) => {
|
|
@@ -139,32 +140,33 @@ export async function serveCommand() {
|
|
|
139
140
|
(async () => {
|
|
140
141
|
for await (const msg of notifSub) {
|
|
141
142
|
const raw = sc.decode(msg.data);
|
|
143
|
+
let parsed;
|
|
142
144
|
try {
|
|
143
|
-
|
|
144
|
-
addNotification({ ...
|
|
145
|
+
parsed = JSON.parse(raw);
|
|
146
|
+
addNotification({ ...parsed, receivedAt: Date.now() });
|
|
145
147
|
}
|
|
146
148
|
catch (err) {
|
|
147
149
|
console.error("[nats] Failed to parse device notification:", err);
|
|
148
150
|
}
|
|
149
|
-
dispatchDeviceEvent("on_new_notification", raw);
|
|
151
|
+
dispatchDeviceEvent("on_new_notification", raw, parsed);
|
|
150
152
|
}
|
|
151
153
|
})();
|
|
152
154
|
const smsSub = nc.subscribe(`host.${config.hostId}.device.sms`);
|
|
153
155
|
(async () => {
|
|
154
156
|
for await (const msg of smsSub) {
|
|
155
157
|
const raw = sc.decode(msg.data);
|
|
158
|
+
let parsed;
|
|
156
159
|
try {
|
|
157
|
-
|
|
158
|
-
addSmsMessage({ ...
|
|
160
|
+
parsed = JSON.parse(raw);
|
|
161
|
+
addSmsMessage({ ...parsed, receivedAt: Date.now() });
|
|
159
162
|
}
|
|
160
163
|
catch (err) {
|
|
161
164
|
console.error("[nats] Failed to parse device SMS:", err);
|
|
162
165
|
}
|
|
163
|
-
dispatchDeviceEvent("on_new_sms", raw);
|
|
166
|
+
dispatchDeviceEvent("on_new_sms", raw, parsed);
|
|
164
167
|
}
|
|
165
168
|
})();
|
|
166
169
|
}
|
|
167
|
-
// Start HTTP transport (loops forever)
|
|
168
170
|
await startHttpTransport(config, handleRpc, httpPort, nc);
|
|
169
171
|
}
|
|
170
172
|
//# sourceMappingURL=serve.js.map
|
package/dist/config.d.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
import type { HostConfig } from "./types.js";
|
|
2
2
|
declare const CONFIG_DIR: string;
|
|
3
3
|
declare const CONFIG_FILE: string;
|
|
4
|
-
/**
|
|
5
|
-
* Load host configuration from ~/.config/palmier/host.json.
|
|
6
|
-
* Throws if the file is missing or invalid.
|
|
7
|
-
*/
|
|
8
4
|
export declare function loadConfig(): HostConfig;
|
|
9
|
-
/**
|
|
10
|
-
* Persist host configuration to ~/.config/palmier/host.json.
|
|
11
|
-
* Creates parent directories if needed.
|
|
12
|
-
*/
|
|
13
5
|
export declare function saveConfig(config: HostConfig): void;
|
|
14
6
|
export { CONFIG_DIR, CONFIG_FILE };
|
|
15
7
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
CHANGED
|
@@ -3,10 +3,6 @@ import * as path from "path";
|
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
const CONFIG_DIR = path.join(homedir(), ".config", "palmier");
|
|
5
5
|
const CONFIG_FILE = path.join(CONFIG_DIR, "host.json");
|
|
6
|
-
/**
|
|
7
|
-
* Load host configuration from ~/.config/palmier/host.json.
|
|
8
|
-
* Throws if the file is missing or invalid.
|
|
9
|
-
*/
|
|
10
6
|
export function loadConfig() {
|
|
11
7
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
12
8
|
throw new Error("Host not provisioned. Run `palmier init` first.\n" +
|
|
@@ -22,10 +18,6 @@ export function loadConfig() {
|
|
|
22
18
|
}
|
|
23
19
|
return config;
|
|
24
20
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Persist host configuration to ~/.config/palmier/host.json.
|
|
27
|
-
* Creates parent directories if needed.
|
|
28
|
-
*/
|
|
29
21
|
export function saveConfig(config) {
|
|
30
22
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
31
23
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
@@ -2,7 +2,7 @@ export interface RegisteredDevice {
|
|
|
2
2
|
clientToken: string;
|
|
3
3
|
fcmToken: string;
|
|
4
4
|
}
|
|
5
|
-
export type DeviceCapability = "location" | "notifications" | "sms" | "contacts" | "calendar" | "
|
|
5
|
+
export type DeviceCapability = "location" | "notifications" | "sms-read" | "sms-send" | "contacts" | "calendar" | "alarm" | "battery" | "send-email" | "dnd";
|
|
6
6
|
export declare function getCapabilityDevice(capability: DeviceCapability): RegisteredDevice | null;
|
|
7
7
|
export declare function setCapabilityDevice(capability: DeviceCapability, clientToken: string, fcmToken: string): void;
|
|
8
8
|
export declare function clearCapabilityDevice(capability: DeviceCapability): void;
|
package/dist/event-queues.d.ts
CHANGED
|
@@ -1,31 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-task in-memory event queues for
|
|
3
|
-
* (schedule_type: "on_new_notification" | "on_new_sms").
|
|
4
|
-
*
|
|
2
|
+
* Per-task in-memory event queues for on_new_notification / on_new_sms schedules.
|
|
5
3
|
* The daemon owns the NATS subscription and populates these queues; the
|
|
6
|
-
* `palmier run` process drains
|
|
7
|
-
* endpoint. `activeRuns` tracks whether a run process is currently draining,
|
|
8
|
-
* so we don't race a fresh startTask with a teardown-phase run.
|
|
4
|
+
* `palmier run` process drains via /task-event/pop.
|
|
9
5
|
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - enqueueEvent returns shouldStart=true only if the task transitioned
|
|
15
|
-
* from idle (no active run) to active — callers must then startTask.
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* Queue a raw (JSON-string) event payload for a task. Returns whether the
|
|
19
|
-
* caller should now start the run process.
|
|
6
|
+
* Invariants:
|
|
7
|
+
* - popEvent clears activeRuns atomically when the queue empties, so a
|
|
8
|
+
* fresh startTask cannot race the tearing-down run.
|
|
9
|
+
* - enqueueEvent returns shouldStart=true only on the idle→active edge.
|
|
20
10
|
*/
|
|
21
11
|
export declare function enqueueEvent(taskId: string, payload: string): {
|
|
22
12
|
shouldStart: boolean;
|
|
23
13
|
};
|
|
24
|
-
/**
|
|
25
|
-
* Pop the oldest queued event for a task. Returns `{ event }` when one is
|
|
26
|
-
* available (keeps the task marked active), or `{ empty: true }` after
|
|
27
|
-
* clearing the active flag atomically.
|
|
28
|
-
*/
|
|
29
14
|
export declare function popEvent(taskId: string): {
|
|
30
15
|
event: string;
|
|
31
16
|
} | {
|
package/dist/event-queues.js
CHANGED
|
@@ -1,26 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-task in-memory event queues for
|
|
3
|
-
* (schedule_type: "on_new_notification" | "on_new_sms").
|
|
4
|
-
*
|
|
2
|
+
* Per-task in-memory event queues for on_new_notification / on_new_sms schedules.
|
|
5
3
|
* The daemon owns the NATS subscription and populates these queues; the
|
|
6
|
-
* `palmier run` process drains
|
|
7
|
-
* endpoint. `activeRuns` tracks whether a run process is currently draining,
|
|
8
|
-
* so we don't race a fresh startTask with a teardown-phase run.
|
|
4
|
+
* `palmier run` process drains via /task-event/pop.
|
|
9
5
|
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - enqueueEvent returns shouldStart=true only if the task transitioned
|
|
15
|
-
* from idle (no active run) to active — callers must then startTask.
|
|
6
|
+
* Invariants:
|
|
7
|
+
* - popEvent clears activeRuns atomically when the queue empties, so a
|
|
8
|
+
* fresh startTask cannot race the tearing-down run.
|
|
9
|
+
* - enqueueEvent returns shouldStart=true only on the idle→active edge.
|
|
16
10
|
*/
|
|
17
11
|
const MAX_QUEUE_SIZE = 100;
|
|
18
12
|
const queues = new Map();
|
|
19
13
|
const activeRuns = new Set();
|
|
20
|
-
/**
|
|
21
|
-
* Queue a raw (JSON-string) event payload for a task. Returns whether the
|
|
22
|
-
* caller should now start the run process.
|
|
23
|
-
*/
|
|
24
14
|
export function enqueueEvent(taskId, payload) {
|
|
25
15
|
const queue = queues.get(taskId) ?? [];
|
|
26
16
|
if (queue.length >= MAX_QUEUE_SIZE)
|
|
@@ -32,11 +22,6 @@ export function enqueueEvent(taskId, payload) {
|
|
|
32
22
|
activeRuns.add(taskId);
|
|
33
23
|
return { shouldStart: true };
|
|
34
24
|
}
|
|
35
|
-
/**
|
|
36
|
-
* Pop the oldest queued event for a task. Returns `{ event }` when one is
|
|
37
|
-
* available (keeps the task marked active), or `{ empty: true }` after
|
|
38
|
-
* clearing the active flag atomically.
|
|
39
|
-
*/
|
|
40
25
|
export function popEvent(taskId) {
|
|
41
26
|
const queue = queues.get(taskId);
|
|
42
27
|
if (queue && queue.length > 0) {
|
package/dist/events.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
1
|
import { type NatsConnection } from "nats";
|
|
2
|
-
/**
|
|
3
|
-
* Broadcast an event to connected clients via NATS and HTTP SSE.
|
|
4
|
-
*
|
|
5
|
-
* - NATS: publishes to `host-event.{hostId}.{taskId}`
|
|
6
|
-
* - HTTP: POSTs to the serve daemon's `/event` endpoint
|
|
7
|
-
*/
|
|
8
2
|
export declare function publishHostEvent(nc: NatsConnection | undefined, hostId: string, taskId: string, payload: Record<string, unknown>): Promise<void>;
|
|
9
3
|
//# sourceMappingURL=events.d.ts.map
|
package/dist/events.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { StringCodec } from "nats";
|
|
2
2
|
import { loadConfig } from "./config.js";
|
|
3
3
|
const sc = StringCodec();
|
|
4
|
-
/**
|
|
5
|
-
* Broadcast an event to connected clients via NATS and HTTP SSE.
|
|
6
|
-
*
|
|
7
|
-
* - NATS: publishes to `host-event.{hostId}.{taskId}`
|
|
8
|
-
* - HTTP: POSTs to the serve daemon's `/event` endpoint
|
|
9
|
-
*/
|
|
10
4
|
export async function publishHostEvent(nc, hostId, taskId, payload) {
|
|
11
5
|
const subject = `host-event.${hostId}.${taskId}`;
|
|
12
6
|
if (nc) {
|
|
@@ -23,8 +17,6 @@ export async function publishHostEvent(nc, hostId, taskId, payload) {
|
|
|
23
17
|
});
|
|
24
18
|
console.log(`[http] host-event: ${taskId} →`, payload);
|
|
25
19
|
}
|
|
26
|
-
catch {
|
|
27
|
-
// Serve HTTP may not be ready yet — ignore
|
|
28
|
-
}
|
|
20
|
+
catch { /* serve HTTP may not be ready yet */ }
|
|
29
21
|
}
|
|
30
22
|
//# sourceMappingURL=events.js.map
|
package/dist/index.js
CHANGED
package/dist/mcp-handler.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import { agentTools, agentToolMap, agentResources, agentResourceMap, ToolError } from "./mcp-tools.js";
|
|
3
|
-
|
|
3
|
+
/** sessionId → subscribed resource URIs */
|
|
4
4
|
const resourceSubscriptions = new Map();
|
|
5
5
|
export function getResourceSubscriptions() {
|
|
6
6
|
return resourceSubscriptions;
|
|
7
7
|
}
|
|
8
|
-
// Session-to-agent name map with 24h TTL
|
|
9
8
|
const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
10
9
|
const sessionAgents = new Map();
|
|
11
10
|
export function getAgentName(sessionId) {
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -37,8 +37,5 @@ export interface ResourceDefinition {
|
|
|
37
37
|
}
|
|
38
38
|
export declare const agentResources: ResourceDefinition[];
|
|
39
39
|
export declare const agentResourceMap: Map<string, ResourceDefinition>;
|
|
40
|
-
/**
|
|
41
|
-
* Generate the HTTP Endpoints markdown section for agent-instructions.md from the tool registry.
|
|
42
|
-
*/
|
|
43
40
|
export declare function generateEndpointDocs(port: number, taskId: string, tools?: ToolDefinition[], resources?: ResourceDefinition[]): string;
|
|
44
41
|
//# sourceMappingURL=mcp-tools.d.ts.map
|