palmier 1.1.20 → 1.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent-tools.d.ts +1 -1
- package/dist/agent-tools.js +36 -48
- package/dist/agents/agent-instructions.md +3 -1
- package/dist/agents/agent.d.ts +1 -1
- package/dist/commands/init.js +2 -0
- package/dist/commands/pair.js +2 -4
- package/dist/commands/run.js +18 -23
- package/dist/commands/serve.js +5 -6
- package/dist/nats-client.d.ts +1 -1
- package/dist/nats-client.js +1 -1
- package/dist/rpc-handler.js +13 -7
- package/dist/task.d.ts +10 -0
- package/dist/task.js +42 -0
- package/dist/transports/http-transport.d.ts +1 -1
- package/dist/transports/http-transport.js +1 -3
- package/dist/transports/nats-transport.d.ts +1 -1
- package/dist/transports/nats-transport.js +3 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -182,7 +182,7 @@ The wizard:
|
|
|
182
182
|
- Offers to install the **Playwright CLI** for browser automation (see [Browser Automation](#browser-automation)). Skipped on re-init if it's already managed.
|
|
183
183
|
- Asks for the HTTP port
|
|
184
184
|
- Detects the default network interface (used for auto-LAN)
|
|
185
|
-
- Shows a summary (including any existing scheduled tasks to recover) and asks for confirmation
|
|
185
|
+
- Shows a summary (including the Palmier daemon version and any existing scheduled tasks to recover) and asks for confirmation
|
|
186
186
|
- Registers with the Palmier server, saves configuration to `~/.config/palmier/host.json`
|
|
187
187
|
- Installs a background daemon (systemd user service on Linux, LaunchAgent on macOS, Task Scheduler on Windows). On re-init, also restarts the daemon so the running process picks up the new agents/versions for `host.info`.
|
|
188
188
|
- Auto-enters pair mode to connect your first device
|
package/dist/agent-tools.d.ts
CHANGED
package/dist/agent-tools.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { StringCodec } from "nats";
|
|
2
1
|
import { registerPending } from "./pending-requests.js";
|
|
3
2
|
import { getLinkedDevice } from "./linked-device.js";
|
|
4
3
|
import { getNotifications } from "./notification-store.js";
|
|
@@ -33,11 +32,10 @@ const notifyTool = {
|
|
|
33
32
|
throw new ToolError("title and body are required", 400);
|
|
34
33
|
if (!ctx.nc)
|
|
35
34
|
throw new ToolError("NATS not connected — push notifications require server mode", 503);
|
|
36
|
-
const sc = StringCodec();
|
|
37
35
|
const payload = { hostId: ctx.config.hostId, title, body, session_id: ctx.taskId };
|
|
38
36
|
const subject = `host.${ctx.config.hostId}.push.send`;
|
|
39
|
-
const reply = await ctx.nc.request(subject,
|
|
40
|
-
const result = JSON.parse(
|
|
37
|
+
const reply = await ctx.nc.request(subject, JSON.stringify(payload), { timeout: 15_000 });
|
|
38
|
+
const result = JSON.parse(reply.string());
|
|
41
39
|
if (result.ok)
|
|
42
40
|
return { ok: true };
|
|
43
41
|
throw new ToolError(result.error ?? "Push notification failed", 502);
|
|
@@ -309,9 +307,8 @@ const deviceGeolocationTool = {
|
|
|
309
307
|
const device = getLinkedDevice();
|
|
310
308
|
if (!device)
|
|
311
309
|
throw new ToolError("No linked device configured", 400);
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
const ack = JSON.parse(sc.decode(ackReply.data));
|
|
310
|
+
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.geolocation`, JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.taskId, fcmToken: device.fcmToken }), { timeout: 5_000 });
|
|
311
|
+
const ack = JSON.parse(ackReply.string());
|
|
315
312
|
if (ack.error)
|
|
316
313
|
throw new ToolError(ack.error, 502);
|
|
317
314
|
const locationPromise = new Promise((resolve, reject) => {
|
|
@@ -323,7 +320,7 @@ const deviceGeolocationTool = {
|
|
|
323
320
|
(async () => {
|
|
324
321
|
for await (const msg of sub) {
|
|
325
322
|
clearTimeout(timer);
|
|
326
|
-
resolve(
|
|
323
|
+
resolve(msg.string());
|
|
327
324
|
}
|
|
328
325
|
})();
|
|
329
326
|
});
|
|
@@ -350,9 +347,8 @@ const readContactsTool = {
|
|
|
350
347
|
const device = getLinkedDevice();
|
|
351
348
|
if (!device)
|
|
352
349
|
throw new ToolError("No linked device configured", 400);
|
|
353
|
-
const
|
|
354
|
-
const
|
|
355
|
-
const ack = JSON.parse(sc.decode(ackReply.data));
|
|
350
|
+
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.contacts`, JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.taskId, fcmToken: device.fcmToken, action: "read" }), { timeout: 5_000 });
|
|
351
|
+
const ack = JSON.parse(ackReply.string());
|
|
356
352
|
if (ack.error)
|
|
357
353
|
throw new ToolError(ack.error, 502);
|
|
358
354
|
const responsePromise = new Promise((resolve, reject) => {
|
|
@@ -364,7 +360,7 @@ const readContactsTool = {
|
|
|
364
360
|
(async () => {
|
|
365
361
|
for await (const msg of sub) {
|
|
366
362
|
clearTimeout(timer);
|
|
367
|
-
resolve(
|
|
363
|
+
resolve(msg.string());
|
|
368
364
|
}
|
|
369
365
|
})();
|
|
370
366
|
});
|
|
@@ -399,12 +395,11 @@ const createContactTool = {
|
|
|
399
395
|
const { name, phone, email } = args;
|
|
400
396
|
if (!name)
|
|
401
397
|
throw new ToolError("name is required", 400);
|
|
402
|
-
const
|
|
403
|
-
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.contacts`, sc.encode(JSON.stringify({
|
|
398
|
+
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.contacts`, JSON.stringify({
|
|
404
399
|
hostId: ctx.config.hostId, requestId: ctx.taskId, fcmToken: device.fcmToken,
|
|
405
400
|
action: "create", name, phone, email,
|
|
406
|
-
})
|
|
407
|
-
const ack = JSON.parse(
|
|
401
|
+
}), { timeout: 5_000 });
|
|
402
|
+
const ack = JSON.parse(ackReply.string());
|
|
408
403
|
if (ack.error)
|
|
409
404
|
throw new ToolError(ack.error, 502);
|
|
410
405
|
const responsePromise = new Promise((resolve, reject) => {
|
|
@@ -416,7 +411,7 @@ const createContactTool = {
|
|
|
416
411
|
(async () => {
|
|
417
412
|
for await (const msg of sub) {
|
|
418
413
|
clearTimeout(timer);
|
|
419
|
-
resolve(
|
|
414
|
+
resolve(msg.string());
|
|
420
415
|
}
|
|
421
416
|
})();
|
|
422
417
|
});
|
|
@@ -448,14 +443,13 @@ const readCalendarTool = {
|
|
|
448
443
|
if (!device)
|
|
449
444
|
throw new ToolError("No linked device configured", 400);
|
|
450
445
|
const { startDate, endDate } = args;
|
|
451
|
-
const
|
|
452
|
-
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.calendar`, sc.encode(JSON.stringify({
|
|
446
|
+
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.calendar`, JSON.stringify({
|
|
453
447
|
hostId: ctx.config.hostId, requestId: ctx.taskId, fcmToken: device.fcmToken,
|
|
454
448
|
action: "read",
|
|
455
449
|
...(startDate ? { startDate: String(startDate) } : {}),
|
|
456
450
|
...(endDate ? { endDate: String(endDate) } : {}),
|
|
457
|
-
})
|
|
458
|
-
const ack = JSON.parse(
|
|
451
|
+
}), { timeout: 5_000 });
|
|
452
|
+
const ack = JSON.parse(ackReply.string());
|
|
459
453
|
if (ack.error)
|
|
460
454
|
throw new ToolError(ack.error, 502);
|
|
461
455
|
const responsePromise = new Promise((resolve, reject) => {
|
|
@@ -467,7 +461,7 @@ const readCalendarTool = {
|
|
|
467
461
|
(async () => {
|
|
468
462
|
for await (const msg of sub) {
|
|
469
463
|
clearTimeout(timer);
|
|
470
|
-
resolve(
|
|
464
|
+
resolve(msg.string());
|
|
471
465
|
}
|
|
472
466
|
})();
|
|
473
467
|
});
|
|
@@ -504,15 +498,14 @@ const createCalendarEventTool = {
|
|
|
504
498
|
const { title, startTime, endTime, location, description } = args;
|
|
505
499
|
if (!title || !startTime || !endTime)
|
|
506
500
|
throw new ToolError("title, startTime, and endTime are required", 400);
|
|
507
|
-
const
|
|
508
|
-
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.calendar`, sc.encode(JSON.stringify({
|
|
501
|
+
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.calendar`, JSON.stringify({
|
|
509
502
|
hostId: ctx.config.hostId, requestId: ctx.taskId, fcmToken: device.fcmToken,
|
|
510
503
|
action: "create",
|
|
511
504
|
title, startTime: String(startTime), endTime: String(endTime),
|
|
512
505
|
...(location ? { location } : {}),
|
|
513
506
|
...(description ? { description } : {}),
|
|
514
|
-
})
|
|
515
|
-
const ack = JSON.parse(
|
|
507
|
+
}), { timeout: 5_000 });
|
|
508
|
+
const ack = JSON.parse(ackReply.string());
|
|
516
509
|
if (ack.error)
|
|
517
510
|
throw new ToolError(ack.error, 502);
|
|
518
511
|
const responsePromise = new Promise((resolve, reject) => {
|
|
@@ -524,7 +517,7 @@ const createCalendarEventTool = {
|
|
|
524
517
|
(async () => {
|
|
525
518
|
for await (const msg of sub) {
|
|
526
519
|
clearTimeout(timer);
|
|
527
|
-
resolve(
|
|
520
|
+
resolve(msg.string());
|
|
528
521
|
}
|
|
529
522
|
})();
|
|
530
523
|
});
|
|
@@ -558,12 +551,11 @@ const sendSmsTool = {
|
|
|
558
551
|
const { to, body } = args;
|
|
559
552
|
if (!to || !body)
|
|
560
553
|
throw new ToolError("to and body are required", 400);
|
|
561
|
-
const
|
|
562
|
-
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.sms`, sc.encode(JSON.stringify({
|
|
554
|
+
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.sms`, JSON.stringify({
|
|
563
555
|
hostId: ctx.config.hostId, requestId: ctx.taskId, fcmToken: device.fcmToken,
|
|
564
556
|
action: "send", to, body,
|
|
565
|
-
})
|
|
566
|
-
const ack = JSON.parse(
|
|
557
|
+
}), { timeout: 5_000 });
|
|
558
|
+
const ack = JSON.parse(ackReply.string());
|
|
567
559
|
if (ack.error)
|
|
568
560
|
throw new ToolError(ack.error, 502);
|
|
569
561
|
const responsePromise = new Promise((resolve, reject) => {
|
|
@@ -575,7 +567,7 @@ const sendSmsTool = {
|
|
|
575
567
|
(async () => {
|
|
576
568
|
for await (const msg of sub) {
|
|
577
569
|
clearTimeout(timer);
|
|
578
|
-
resolve(
|
|
570
|
+
resolve(msg.string());
|
|
579
571
|
}
|
|
580
572
|
})();
|
|
581
573
|
});
|
|
@@ -610,15 +602,14 @@ const sendAlarmTool = {
|
|
|
610
602
|
const { title, description } = args;
|
|
611
603
|
if (!title)
|
|
612
604
|
throw new ToolError("title is required", 400);
|
|
613
|
-
const sc = StringCodec();
|
|
614
605
|
const payload = {
|
|
615
606
|
hostId: ctx.config.hostId, requestId: ctx.taskId, fcmToken: device.fcmToken,
|
|
616
607
|
title,
|
|
617
608
|
};
|
|
618
609
|
if (description)
|
|
619
610
|
payload.description = description;
|
|
620
|
-
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.alarm`,
|
|
621
|
-
const ack = JSON.parse(
|
|
611
|
+
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.alarm`, JSON.stringify(payload), { timeout: 5_000 });
|
|
612
|
+
const ack = JSON.parse(ackReply.string());
|
|
622
613
|
if (ack.error)
|
|
623
614
|
throw new ToolError(ack.error, 502);
|
|
624
615
|
const responsePromise = new Promise((resolve, reject) => {
|
|
@@ -630,7 +621,7 @@ const sendAlarmTool = {
|
|
|
630
621
|
(async () => {
|
|
631
622
|
for await (const msg of sub) {
|
|
632
623
|
clearTimeout(timer);
|
|
633
|
-
resolve(
|
|
624
|
+
resolve(msg.string());
|
|
634
625
|
}
|
|
635
626
|
})();
|
|
636
627
|
});
|
|
@@ -657,9 +648,8 @@ const readBatteryTool = {
|
|
|
657
648
|
const device = getLinkedDevice();
|
|
658
649
|
if (!device)
|
|
659
650
|
throw new ToolError("No linked device configured", 400);
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
const ack = JSON.parse(sc.decode(ackReply.data));
|
|
651
|
+
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.battery`, JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.taskId, fcmToken: device.fcmToken }), { timeout: 5_000 });
|
|
652
|
+
const ack = JSON.parse(ackReply.string());
|
|
663
653
|
if (ack.error)
|
|
664
654
|
throw new ToolError(ack.error, 502);
|
|
665
655
|
const responsePromise = new Promise((resolve, reject) => {
|
|
@@ -671,7 +661,7 @@ const readBatteryTool = {
|
|
|
671
661
|
(async () => {
|
|
672
662
|
for await (const msg of sub) {
|
|
673
663
|
clearTimeout(timer);
|
|
674
|
-
resolve(
|
|
664
|
+
resolve(msg.string());
|
|
675
665
|
}
|
|
676
666
|
})();
|
|
677
667
|
});
|
|
@@ -704,9 +694,8 @@ const setRingerModeTool = {
|
|
|
704
694
|
const { mode } = args;
|
|
705
695
|
if (!["normal", "vibrate", "silent"].includes(mode))
|
|
706
696
|
throw new ToolError("mode must be 'normal', 'vibrate', or 'silent'", 400);
|
|
707
|
-
const
|
|
708
|
-
const
|
|
709
|
-
const ack = JSON.parse(sc.decode(ackReply.data));
|
|
697
|
+
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.ringer`, JSON.stringify({ hostId: ctx.config.hostId, requestId: ctx.taskId, fcmToken: device.fcmToken, mode }), { timeout: 5_000 });
|
|
698
|
+
const ack = JSON.parse(ackReply.string());
|
|
710
699
|
if (ack.error)
|
|
711
700
|
throw new ToolError(ack.error, 502);
|
|
712
701
|
const responsePromise = new Promise((resolve, reject) => {
|
|
@@ -718,7 +707,7 @@ const setRingerModeTool = {
|
|
|
718
707
|
(async () => {
|
|
719
708
|
for await (const msg of sub) {
|
|
720
709
|
clearTimeout(timer);
|
|
721
|
-
resolve(
|
|
710
|
+
resolve(msg.string());
|
|
722
711
|
}
|
|
723
712
|
})();
|
|
724
713
|
});
|
|
@@ -755,7 +744,6 @@ const sendEmailTool = {
|
|
|
755
744
|
const { to, subject, body, cc, bcc } = args;
|
|
756
745
|
if (!to)
|
|
757
746
|
throw new ToolError("to is required", 400);
|
|
758
|
-
const sc = StringCodec();
|
|
759
747
|
const payload = {
|
|
760
748
|
hostId: ctx.config.hostId, requestId: ctx.taskId, fcmToken: device.fcmToken,
|
|
761
749
|
to,
|
|
@@ -768,8 +756,8 @@ const sendEmailTool = {
|
|
|
768
756
|
payload.cc = cc;
|
|
769
757
|
if (bcc)
|
|
770
758
|
payload.bcc = bcc;
|
|
771
|
-
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.email`,
|
|
772
|
-
const ack = JSON.parse(
|
|
759
|
+
const ackReply = await ctx.nc.request(`host.${ctx.config.hostId}.fcm.email`, JSON.stringify(payload), { timeout: 5_000 });
|
|
760
|
+
const ack = JSON.parse(ackReply.string());
|
|
773
761
|
if (ack.error)
|
|
774
762
|
throw new ToolError(ack.error, 502);
|
|
775
763
|
const responsePromise = new Promise((resolve, reject) => {
|
|
@@ -781,7 +769,7 @@ const sendEmailTool = {
|
|
|
781
769
|
(async () => {
|
|
782
770
|
for await (const msg of sub) {
|
|
783
771
|
clearTimeout(timer);
|
|
784
|
-
resolve(
|
|
772
|
+
resolve(msg.string());
|
|
785
773
|
}
|
|
786
774
|
})();
|
|
787
775
|
});
|
|
@@ -29,7 +29,9 @@ Whenever a tool you are trying to use is denied or you lack the required permiss
|
|
|
29
29
|
|
|
30
30
|
## Browsers
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Whenever the task needs a browser, launch it with the Playwright CLI (`playwright-cli open`). Do not use any other browser tool, automation library, or launch method.
|
|
33
|
+
|
|
34
|
+
When launching, always pass `--headed` (never run headless) and `--persistent` (reuse a persistent profile so logins and cookies survive across runs).
|
|
33
35
|
|
|
34
36
|
When the task is finished, close any browser you opened with the Playwright CLI `close` command (pass the same `-s=<session>` if you used one) before printing the completion marker.
|
|
35
37
|
|
package/dist/agents/agent.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface AgentTool {
|
|
|
30
30
|
supportsPermissions?: boolean;
|
|
31
31
|
/** Whether this agent supports yolo mode (auto-approve all tools). */
|
|
32
32
|
supportsYolo?: boolean;
|
|
33
|
-
/** When true, the run loop
|
|
33
|
+
/** When true, the run loop persists the agent's stderr output only if the invocation fails. */
|
|
34
34
|
suppressStdErr?: boolean;
|
|
35
35
|
/** Matches the model name (capture group 1) in the banner the CLI prints on stderr.
|
|
36
36
|
* Takes precedence over the agent's self-reported model marker. */
|
package/dist/commands/init.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getPlatform } from "../platform/index.js";
|
|
|
10
10
|
import { pairCommand } from "./pair.js";
|
|
11
11
|
import { detectDefaultInterface, getInterfaceIpv4 } from "../network.js";
|
|
12
12
|
import { listTasks, taskDisplayName } from "../task.js";
|
|
13
|
+
import { currentVersion } from "../update-checker.js";
|
|
13
14
|
const { bold, dim, green, cyan, red } = colors;
|
|
14
15
|
export async function initCommand() {
|
|
15
16
|
console.log(`\n${bold("=== Palmier Host Setup ===")}\n`);
|
|
@@ -74,6 +75,7 @@ export async function initCommand() {
|
|
|
74
75
|
const defaultInterface = (await detectDefaultInterface()) ?? undefined;
|
|
75
76
|
const lanIp = defaultInterface ? getInterfaceIpv4(defaultInterface) : null;
|
|
76
77
|
console.log(`\n${bold("Setup summary:")}\n`);
|
|
78
|
+
console.log(` ${dim("Daemon:")} Palmier v${currentVersion}\n`);
|
|
77
79
|
console.log(` ${dim("Task storage:")} ${bold(projectRoot)}`);
|
|
78
80
|
console.log(` All tasks and execution data will be stored here.\n`);
|
|
79
81
|
console.log(` ${dim("Local:")} ${cyan(`http://localhost:${httpPort}`)}`);
|
package/dist/commands/pair.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
2
|
import * as os from "node:os";
|
|
3
|
-
import { StringCodec } from "nats";
|
|
4
3
|
import { loadConfig } from "../config.js";
|
|
5
4
|
import { connectNats } from "../nats-client.js";
|
|
6
5
|
import { addClient } from "../client-store.js";
|
|
@@ -66,7 +65,6 @@ export async function pairCommand() {
|
|
|
66
65
|
console.log("");
|
|
67
66
|
console.log("Code expires in 1 minute.");
|
|
68
67
|
const nc = await connectNats(config);
|
|
69
|
-
const sc = StringCodec();
|
|
70
68
|
const subject = `pair.${code}`;
|
|
71
69
|
const sub = nc.subscribe(subject, { max: 1 });
|
|
72
70
|
cleanups.push(() => {
|
|
@@ -80,14 +78,14 @@ export async function pairCommand() {
|
|
|
80
78
|
let label;
|
|
81
79
|
try {
|
|
82
80
|
if (msg.data && msg.data.length > 0) {
|
|
83
|
-
const body = JSON.parse(
|
|
81
|
+
const body = JSON.parse(msg.string());
|
|
84
82
|
label = body.label;
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
catch { /* empty body is fine */ }
|
|
88
86
|
const response = buildPairResponse(config, label);
|
|
89
87
|
if (msg.reply) {
|
|
90
|
-
msg.respond(
|
|
88
|
+
msg.respond(JSON.stringify(response));
|
|
91
89
|
}
|
|
92
90
|
onPaired();
|
|
93
91
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { spawnCommand } from "../spawn-command.js";
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
|
-
import { parseTaskFile, taskDisplayName, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createRunDir,
|
|
5
|
+
import { parseTaskFile, taskDisplayName, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createRunDir, claimPendingRun, appendRunMessage, getRunDir, beginStreamingMessage, formatAgentModelComment } from "../task.js";
|
|
6
6
|
import { getAgent } from "../agents/agent.js";
|
|
7
7
|
import { getPlatform } from "../platform/index.js";
|
|
8
8
|
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_PERMISSION_PREFIX, AGENT_MODEL_PREFIX } from "../agents/shared-prompt.js";
|
|
@@ -19,6 +19,7 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
19
19
|
let writer;
|
|
20
20
|
let activeStream;
|
|
21
21
|
const lineBufs = { stdout: "", stderr: "" };
|
|
22
|
+
let suppressedStderr = "";
|
|
22
23
|
let notifyPending = false;
|
|
23
24
|
let notifyTimer;
|
|
24
25
|
let agentModel;
|
|
@@ -50,7 +51,9 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
50
51
|
throttledNotify();
|
|
51
52
|
}
|
|
52
53
|
function onStderr(chunk) {
|
|
53
|
-
if (
|
|
54
|
+
if (ctx.agent.suppressStdErr)
|
|
55
|
+
suppressedStderr += chunk;
|
|
56
|
+
else
|
|
54
57
|
emit("stderr", chunk);
|
|
55
58
|
const cliModel = captureCliModel(chunk);
|
|
56
59
|
if (cliModel && !agentModelWritten)
|
|
@@ -71,6 +74,12 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
71
74
|
}
|
|
72
75
|
flushAgentModel();
|
|
73
76
|
}
|
|
77
|
+
function flushTrailingLine(stream) {
|
|
78
|
+
const trailing = lineBufs[stream];
|
|
79
|
+
if (trailing && !trailing.startsWith("[PALMIER")) {
|
|
80
|
+
ensureWriter(stream).write(trailing);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
74
83
|
const { args, stdin, env: agentEnv, files } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions);
|
|
75
84
|
const command = ctx.agent.command;
|
|
76
85
|
const runDir = getRunDir(ctx.taskDir, ctx.runId);
|
|
@@ -87,21 +96,19 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
|
|
|
87
96
|
onStdout: (chunk) => emit("stdout", chunk),
|
|
88
97
|
onStderr,
|
|
89
98
|
});
|
|
90
|
-
if (notifyTimer)
|
|
91
|
-
clearTimeout(notifyTimer);
|
|
92
99
|
const outcome = result.exitCode !== 0 ? "failed" : parseTaskOutcome(result.output);
|
|
93
100
|
const linkedFiles = listLinkedTaskFiles(ctx.taskDir, ctx.runId, result.output);
|
|
94
101
|
const requiredPermissions = parsePermissions(result.output);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
}
|
|
102
|
+
flushTrailingLine("stdout");
|
|
103
|
+
if (outcome === "failed")
|
|
104
|
+
emit("stderr", suppressedStderr);
|
|
105
|
+
flushTrailingLine("stderr");
|
|
101
106
|
if (requiredPermissions.length > 0) {
|
|
102
107
|
const permLines = requiredPermissions.map((p) => `- **${p.name}** ${p.description}`).join("\n");
|
|
103
108
|
ensureWriter("stdout").write(`\n\n**Permissions requested:**\n${permLines}\n`);
|
|
104
109
|
}
|
|
110
|
+
if (notifyTimer)
|
|
111
|
+
clearTimeout(notifyTimer);
|
|
105
112
|
writer?.end();
|
|
106
113
|
await publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
107
114
|
if (linkedFiles.length > 0) {
|
|
@@ -154,18 +161,6 @@ async function appendAndNotify(ctx, msg) {
|
|
|
154
161
|
appendRunMessage(ctx.taskDir, ctx.runId, msg);
|
|
155
162
|
await publishHostEvent(ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
156
163
|
}
|
|
157
|
-
/** The latest run dir with no status messages yet — freshly created by the RPC handler. */
|
|
158
|
-
function findLatestPendingRunId(taskDir) {
|
|
159
|
-
const dirs = fs.readdirSync(taskDir)
|
|
160
|
-
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
|
|
161
|
-
.sort();
|
|
162
|
-
if (dirs.length === 0)
|
|
163
|
-
return null;
|
|
164
|
-
const latest = dirs[dirs.length - 1];
|
|
165
|
-
const messages = readRunMessages(taskDir, latest);
|
|
166
|
-
const hasStatus = messages.some((m) => m.role === "status");
|
|
167
|
-
return hasStatus ? null : latest;
|
|
168
|
-
}
|
|
169
164
|
/**
|
|
170
165
|
* If the RPC handler already wrote "aborted" (via task.abort), respect that
|
|
171
166
|
* instead of overwriting with the process's own outcome.
|
|
@@ -182,7 +177,7 @@ export async function runCommand(taskId) {
|
|
|
182
177
|
const task = parseTaskFile(taskDir);
|
|
183
178
|
console.log(`Running task: ${taskId}`);
|
|
184
179
|
const taskName = taskDisplayName(task.frontmatter);
|
|
185
|
-
const existingRunId =
|
|
180
|
+
const existingRunId = claimPendingRun(taskDir);
|
|
186
181
|
const agentVersion = config.agents?.find((a) => a.key === task.frontmatter.agent)?.version;
|
|
187
182
|
const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now(), task.frontmatter.agent, agentVersion);
|
|
188
183
|
if (!existingRunId) {
|
package/dist/commands/serve.js
CHANGED
|
@@ -12,7 +12,6 @@ import { detectAgents } from "../agents/agent.js";
|
|
|
12
12
|
import { getPlaywrightCliVersion } from "../playwright-cli.js";
|
|
13
13
|
import { saveConfig } from "../config.js";
|
|
14
14
|
import { CONFIG_DIR } from "../config.js";
|
|
15
|
-
import { StringCodec } from "nats";
|
|
16
15
|
import { addNotification } from "../notification-store.js";
|
|
17
16
|
import { addSmsMessage } from "../sms-store.js";
|
|
18
17
|
import { dispatchTrigger } from "../trigger-dispatch.js";
|
|
@@ -118,13 +117,13 @@ export async function serveCommand() {
|
|
|
118
117
|
console.log("[nats] Connected");
|
|
119
118
|
(async () => {
|
|
120
119
|
for await (const s of conn.status()) {
|
|
121
|
-
if (s.type === "
|
|
120
|
+
if (s.type === "ping")
|
|
122
121
|
continue;
|
|
123
|
-
|
|
122
|
+
const detail = "server" in s ? s.server : "error" in s ? s.error.message : undefined;
|
|
123
|
+
console.log(`[nats] Status: ${s.type}${detail ? ` (${detail})` : ""}`);
|
|
124
124
|
}
|
|
125
125
|
})();
|
|
126
126
|
startNatsTransport(config, handleRpc, conn);
|
|
127
|
-
const sc = StringCodec();
|
|
128
127
|
// Match phone numbers regardless of formatting; letters preserved for shortcodes.
|
|
129
128
|
function normalizeSender(raw) {
|
|
130
129
|
return raw.replace(/[\s\-()+]/g, "").toLowerCase();
|
|
@@ -152,7 +151,7 @@ export async function serveCommand() {
|
|
|
152
151
|
const notifSub = conn.subscribe(`host.${config.hostId}.device.notifications`);
|
|
153
152
|
(async () => {
|
|
154
153
|
for await (const msg of notifSub) {
|
|
155
|
-
const raw =
|
|
154
|
+
const raw = msg.string();
|
|
156
155
|
let parsed;
|
|
157
156
|
try {
|
|
158
157
|
parsed = JSON.parse(raw);
|
|
@@ -167,7 +166,7 @@ export async function serveCommand() {
|
|
|
167
166
|
const smsSub = conn.subscribe(`host.${config.hostId}.device.sms`);
|
|
168
167
|
(async () => {
|
|
169
168
|
for await (const msg of smsSub) {
|
|
170
|
-
const raw =
|
|
169
|
+
const raw = msg.string();
|
|
171
170
|
let parsed;
|
|
172
171
|
try {
|
|
173
172
|
parsed = JSON.parse(raw);
|
package/dist/nats-client.d.ts
CHANGED
package/dist/nats-client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { connect, jwtAuthenticator } from "nats";
|
|
1
|
+
import { connect, jwtAuthenticator } from "@nats-io/transport-node";
|
|
2
2
|
export async function connectNats(config) {
|
|
3
3
|
if (!config.natsJwt || !config.natsNkeySeed) {
|
|
4
4
|
throw new Error("NATS JWT credentials not configured. Re-run palmier init.");
|
package/dist/rpc-handler.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import { listTasks, parseTaskFile, taskDisplayName, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, isTaskInList,
|
|
4
|
+
import { listTasks, parseTaskFile, taskDisplayName, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, isTaskInList, createPendingRun, readPendingRun, appendRunMessage, getRunDir, writeFollowupStatus, readFollowupStatus, deleteFollowupStatus, isRunStarred, setRunStarred, formatAgentModelComment, extractAgentModelComment } from "./task.js";
|
|
5
5
|
import { resolvePending, getPending, listPending } from "./pending-requests.js";
|
|
6
6
|
import { getPlatform } from "./platform/index.js";
|
|
7
7
|
import { onChildDone, resolveCommand, spawnCommand } from "./spawn-command.js";
|
|
@@ -136,6 +136,8 @@ async function generateName(projectRoot, userPrompt, agentName) {
|
|
|
136
136
|
return userPrompt;
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
+
// Longer than the slowest observed launch (~5s on Windows) before the run process claims its run.
|
|
140
|
+
const DUPLICATE_RUN_WINDOW_MS = 15_000;
|
|
139
141
|
/** Active follow-up child processes, keyed by "taskId:runId". */
|
|
140
142
|
const activeFollowups = new Map();
|
|
141
143
|
/**
|
|
@@ -364,8 +366,7 @@ export function createRpcHandler(config) {
|
|
|
364
366
|
writeTaskFile(taskDir, task);
|
|
365
367
|
// One-off run: do NOT append to tasks.jsonl.
|
|
366
368
|
const oneoffAgentVersion = config.agents?.find((a) => a.key === params.agent)?.version;
|
|
367
|
-
const runId =
|
|
368
|
-
appendHistory(config.projectRoot, { task_id: id, run_id: runId });
|
|
369
|
+
const runId = createPendingRun(config.projectRoot, taskDir, id, name, params.agent, oneoffAgentVersion);
|
|
369
370
|
const platform = getPlatform();
|
|
370
371
|
platform.installTaskTimer(config, task);
|
|
371
372
|
try {
|
|
@@ -382,17 +383,22 @@ export function createRpcHandler(config) {
|
|
|
382
383
|
case "task.run": {
|
|
383
384
|
const params = request.params;
|
|
384
385
|
const runTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
386
|
+
const launchingRunId = readPendingRun(runTaskDir);
|
|
387
|
+
if (launchingRunId && Date.now() - Number(launchingRunId) < DUPLICATE_RUN_WINDOW_MS) {
|
|
388
|
+
console.log(`[task.run] Task ${params.id} run ${launchingRunId} is still launching, ignoring duplicate request`);
|
|
389
|
+
return { ok: true, task_id: params.id, run_id: launchingRunId };
|
|
390
|
+
}
|
|
385
391
|
let taskRunId;
|
|
386
392
|
try {
|
|
387
393
|
const platform = getPlatform();
|
|
394
|
+
// Must stay synchronous up to here so a concurrent duplicate sees the pending run.
|
|
395
|
+
const runTask = parseTaskFile(runTaskDir);
|
|
396
|
+
const taskRunAgentVersion = config.agents?.find((a) => a.key === runTask.frontmatter.agent)?.version;
|
|
397
|
+
taskRunId = createPendingRun(config.projectRoot, runTaskDir, params.id, taskDisplayName(runTask.frontmatter), runTask.frontmatter.agent, taskRunAgentVersion);
|
|
388
398
|
if (platform.isTaskRunning(params.id)) {
|
|
389
399
|
console.log(`[task.run] Task ${params.id} is already running, killing stale process`);
|
|
390
400
|
await platform.stopTask(params.id);
|
|
391
401
|
}
|
|
392
|
-
const runTask = parseTaskFile(runTaskDir);
|
|
393
|
-
const taskRunAgentVersion = config.agents?.find((a) => a.key === runTask.frontmatter.agent)?.version;
|
|
394
|
-
taskRunId = createRunDir(runTaskDir, taskDisplayName(runTask.frontmatter), Date.now(), runTask.frontmatter.agent, taskRunAgentVersion);
|
|
395
|
-
appendHistory(config.projectRoot, { task_id: params.id, run_id: taskRunId });
|
|
396
402
|
await platform.startTask(params.id);
|
|
397
403
|
return { ok: true, task_id: params.id, run_id: taskRunId };
|
|
398
404
|
}
|
package/dist/task.d.ts
CHANGED
|
@@ -21,6 +21,16 @@ export declare function isRunStarred(runDir: string): boolean;
|
|
|
21
21
|
export declare function setRunStarred(runDir: string, starred: boolean): void;
|
|
22
22
|
/** Returns the run ID (timestamp string used as directory name). */
|
|
23
23
|
export declare function createRunDir(taskDir: string, taskName: string, startTime: number, agent?: string, agentVersion?: string): string;
|
|
24
|
+
/** The run dir created for the next run process that it has not claimed yet, or null. */
|
|
25
|
+
export declare function readPendingRun(taskDir: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Create a run dir (plus its history entry) for the run process about to be
|
|
28
|
+
* launched to claim. A previous pending run that was never claimed is discarded
|
|
29
|
+
* so it doesn't linger as an empty session.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createPendingRun(projectRoot: string, taskDir: string, taskId: string, taskName: string, agent?: string, agentVersion?: string): string;
|
|
32
|
+
/** Take ownership of the pending run; null if there is none or another process took it first. */
|
|
33
|
+
export declare function claimPendingRun(taskDir: string): string | null;
|
|
24
34
|
export declare function getRunDir(taskDir: string, runId: string): string;
|
|
25
35
|
export declare function appendRunMessage(taskDir: string, runId: string, msg: ConversationMessage): void;
|
|
26
36
|
export declare function beginStreamingMessage(taskDir: string, runId: string, time: number, stream?: "stdout" | "stderr"): StreamingMessageWriter;
|
package/dist/task.js
CHANGED
|
@@ -155,6 +155,48 @@ export function createRunDir(taskDir, taskName, startTime, agent, agentVersion)
|
|
|
155
155
|
fs.writeFileSync(path.join(runDir, "TASKRUN.md"), content, "utf-8");
|
|
156
156
|
return runId;
|
|
157
157
|
}
|
|
158
|
+
const PENDING_RUN_FILE = "pending-run";
|
|
159
|
+
function isUnclaimedRun(taskDir, runId) {
|
|
160
|
+
if (!/^\d+$/.test(runId) || !fs.existsSync(path.join(taskDir, runId, "TASKRUN.md")))
|
|
161
|
+
return false;
|
|
162
|
+
return !readRunMessages(taskDir, runId).some((m) => m.role === "status");
|
|
163
|
+
}
|
|
164
|
+
/** The run dir created for the next run process that it has not claimed yet, or null. */
|
|
165
|
+
export function readPendingRun(taskDir) {
|
|
166
|
+
let runId;
|
|
167
|
+
try {
|
|
168
|
+
runId = fs.readFileSync(path.join(taskDir, PENDING_RUN_FILE), "utf-8").trim();
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return isUnclaimedRun(taskDir, runId) ? runId : null;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Create a run dir (plus its history entry) for the run process about to be
|
|
177
|
+
* launched to claim. A previous pending run that was never claimed is discarded
|
|
178
|
+
* so it doesn't linger as an empty session.
|
|
179
|
+
*/
|
|
180
|
+
export function createPendingRun(projectRoot, taskDir, taskId, taskName, agent, agentVersion) {
|
|
181
|
+
const stale = readPendingRun(taskDir);
|
|
182
|
+
if (stale)
|
|
183
|
+
deleteHistoryEntry(projectRoot, taskId, stale);
|
|
184
|
+
const runId = createRunDir(taskDir, taskName, Date.now(), agent, agentVersion);
|
|
185
|
+
appendHistory(projectRoot, { task_id: taskId, run_id: runId });
|
|
186
|
+
fs.writeFileSync(path.join(taskDir, PENDING_RUN_FILE), runId, "utf-8");
|
|
187
|
+
return runId;
|
|
188
|
+
}
|
|
189
|
+
/** Take ownership of the pending run; null if there is none or another process took it first. */
|
|
190
|
+
export function claimPendingRun(taskDir) {
|
|
191
|
+
const runId = readPendingRun(taskDir);
|
|
192
|
+
try {
|
|
193
|
+
fs.unlinkSync(path.join(taskDir, PENDING_RUN_FILE));
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return runId;
|
|
199
|
+
}
|
|
158
200
|
export function getRunDir(taskDir, runId) {
|
|
159
201
|
return path.join(taskDir, runId);
|
|
160
202
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { type NatsConnection } from "nats";
|
|
1
|
+
import { type NatsConnection } from "@nats-io/transport-node";
|
|
2
2
|
import type { HostConfig, RpcMessage } from "../types.js";
|
|
3
3
|
export declare function startHttpTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>, port: number, getNc: () => NatsConnection | undefined, pairingCode?: string, onReady?: () => void): Promise<void>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { StringCodec } from "nats";
|
|
5
4
|
import { validateClient, addClient } from "../client-store.js";
|
|
6
5
|
import { registerPending } from "../pending-requests.js";
|
|
7
6
|
import * as fs from "node:fs";
|
|
@@ -106,9 +105,8 @@ export async function startHttpTransport(config, handleRpc, port, getNc, pairing
|
|
|
106
105
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
107
106
|
}
|
|
108
107
|
async function publishEvent(taskId, payload) {
|
|
109
|
-
const sc = StringCodec();
|
|
110
108
|
const subject = `host-event.${config.hostId}.${taskId}`;
|
|
111
|
-
getNc()?.publish(subject,
|
|
109
|
+
getNc()?.publish(subject, JSON.stringify(payload));
|
|
112
110
|
broadcastSseEvent({ task_id: taskId, ...payload });
|
|
113
111
|
}
|
|
114
112
|
function makeToolContext(taskId) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { type NatsConnection } from "nats";
|
|
1
|
+
import { type NatsConnection } from "@nats-io/transport-node";
|
|
2
2
|
import type { HostConfig, RpcMessage } from "../types.js";
|
|
3
3
|
export declare function startNatsTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>, nc: NatsConnection): Promise<void>;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { StringCodec } from "nats";
|
|
2
1
|
export async function startNatsTransport(config, handleRpc, nc) {
|
|
3
|
-
const sc = StringCodec();
|
|
4
2
|
const subject = `host.${config.hostId}.rpc.>`;
|
|
5
3
|
console.log(`[nats] Subscribing to: ${subject}`);
|
|
6
4
|
const sub = nc.subscribe(subject);
|
|
@@ -19,7 +17,7 @@ export async function startNatsTransport(config, handleRpc, nc) {
|
|
|
19
17
|
const method = rpcIdx >= 0 ? subjectTokens.slice(rpcIdx + 1).join(".") : "";
|
|
20
18
|
let params = {};
|
|
21
19
|
if (msg.data && msg.data.length > 0) {
|
|
22
|
-
const raw =
|
|
20
|
+
const raw = msg.string().trim();
|
|
23
21
|
if (raw.length > 0) {
|
|
24
22
|
try {
|
|
25
23
|
params = JSON.parse(raw);
|
|
@@ -27,7 +25,7 @@ export async function startNatsTransport(config, handleRpc, nc) {
|
|
|
27
25
|
catch {
|
|
28
26
|
console.error(`[nats] Failed to parse RPC params for ${method}`);
|
|
29
27
|
if (msg.reply) {
|
|
30
|
-
msg.respond(
|
|
28
|
+
msg.respond(JSON.stringify({ error: "Invalid JSON" }));
|
|
31
29
|
}
|
|
32
30
|
return;
|
|
33
31
|
}
|
|
@@ -47,7 +45,7 @@ export async function startNatsTransport(config, handleRpc, nc) {
|
|
|
47
45
|
}
|
|
48
46
|
console.log(`[nats] RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
|
|
49
47
|
if (msg.reply) {
|
|
50
|
-
msg.respond(
|
|
48
|
+
msg.respond(JSON.stringify(response));
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
51
|
async function consumeSubscription(subscription) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "palmier",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.22",
|
|
4
4
|
"description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Hongxu Cai",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"start": "node dist/index.js"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@nats-io/transport-node": "^3.4.0",
|
|
39
40
|
"commander": "^13.1.0",
|
|
40
41
|
"cross-spawn": "^7.0.6",
|
|
41
42
|
"dotenv": "^16.4.7",
|
|
42
|
-
"nats": "^2.29.1",
|
|
43
43
|
"yaml": "^2.7.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|