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/src/platform/windows.ts
CHANGED
|
@@ -33,32 +33,25 @@ export function scheduleValueToXml(scheduleType: "crons" | "specific_times", val
|
|
|
33
33
|
if (parts.length !== 5) throw new Error(`Invalid cron expression: ${value}`);
|
|
34
34
|
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
|
|
35
35
|
const st = `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:00`;
|
|
36
|
-
// StartBoundary needs a full date;
|
|
36
|
+
// StartBoundary needs a full date; anchor to a past one.
|
|
37
37
|
const base = `2000-01-01T${st}`;
|
|
38
38
|
|
|
39
|
-
// Hourly
|
|
40
39
|
if (hour === "*") {
|
|
41
40
|
return `<TimeTrigger><StartBoundary>${base}</StartBoundary><Repetition><Interval>PT1H</Interval></Repetition></TimeTrigger>`;
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
// Weekly
|
|
45
43
|
if (dayOfMonth === "*" && dayOfWeek !== "*") {
|
|
46
44
|
const day = DOW_NAMES[Number(dayOfWeek)] ?? "Monday";
|
|
47
45
|
return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByWeek><DaysOfWeek><${day} /></DaysOfWeek><WeeksInterval>1</WeeksInterval></ScheduleByWeek></CalendarTrigger>`;
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
// Monthly
|
|
51
48
|
if (dayOfMonth !== "*" && dayOfWeek === "*") {
|
|
52
49
|
return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByMonth><DaysOfMonth><Day>${dayOfMonth}</Day></DaysOfMonth><Months><January /><February /><March /><April /><May /><June /><July /><August /><September /><October /><November /><December /></Months></ScheduleByMonth></CalendarTrigger>`;
|
|
53
50
|
}
|
|
54
51
|
|
|
55
|
-
// Daily
|
|
56
52
|
return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByDay><DaysInterval>1</DaysInterval></ScheduleByDay></CalendarTrigger>`;
|
|
57
53
|
}
|
|
58
54
|
|
|
59
|
-
/**
|
|
60
|
-
* Build a complete Task Scheduler XML definition.
|
|
61
|
-
*/
|
|
62
55
|
export function buildTaskXml(tr: string, triggers: string[], foreground?: boolean): string {
|
|
63
56
|
const [command, ...argParts] = tr.match(/"[^"]*"|[^\s]+/g) ?? [];
|
|
64
57
|
const commandStr = command?.replace(/"/g, "") ?? "";
|
|
@@ -98,10 +91,7 @@ export class WindowsPlatform implements PlatformService {
|
|
|
98
91
|
installDaemon(config: HostConfig): void {
|
|
99
92
|
const script = process.argv[1] || "palmier";
|
|
100
93
|
|
|
101
|
-
// Create the Task Scheduler entry for the daemon (BootTrigger starts it at system boot)
|
|
102
94
|
this.ensureDaemonTask(script);
|
|
103
|
-
|
|
104
|
-
// Start the daemon now
|
|
105
95
|
this.startDaemonTask();
|
|
106
96
|
|
|
107
97
|
console.log("\nHost initialization complete!");
|
|
@@ -110,12 +100,11 @@ export class WindowsPlatform implements PlatformService {
|
|
|
110
100
|
uninstallDaemon(): void {
|
|
111
101
|
const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
|
|
112
102
|
|
|
113
|
-
// Stop the daemon via Task Scheduler
|
|
114
103
|
try {
|
|
115
104
|
execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
|
|
116
105
|
} catch { /* task may not be running */ }
|
|
117
106
|
|
|
118
|
-
//
|
|
107
|
+
// Deleting an S4U task requires elevation.
|
|
119
108
|
try {
|
|
120
109
|
execFileSync("powershell", [
|
|
121
110
|
"-Command", `Start-Process -Verb RunAs -Wait -FilePath schtasks -ArgumentList '/delete /tn "${tn}" /f'`,
|
|
@@ -123,7 +112,6 @@ export class WindowsPlatform implements PlatformService {
|
|
|
123
112
|
console.log("Daemon task removed.");
|
|
124
113
|
} catch { /* task may not exist */ }
|
|
125
114
|
|
|
126
|
-
// Remove all Palmier task timers
|
|
127
115
|
try {
|
|
128
116
|
const out = execFileSync("schtasks", ["/query", "/fo", "CSV", "/nh"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
|
|
129
117
|
for (const line of out.split("\n")) {
|
|
@@ -142,16 +130,14 @@ export class WindowsPlatform implements PlatformService {
|
|
|
142
130
|
async restartDaemon(): Promise<void> {
|
|
143
131
|
const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
|
|
144
132
|
|
|
145
|
-
// Stop the daemon via Task Scheduler
|
|
146
133
|
try {
|
|
147
134
|
execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
|
|
148
135
|
} catch { /* task may not be running */ }
|
|
149
136
|
|
|
150
|
-
// Start it again
|
|
151
137
|
this.startDaemonTask();
|
|
152
138
|
}
|
|
153
139
|
|
|
154
|
-
/**
|
|
140
|
+
/** S4U LogonType requires elevation to create. */
|
|
155
141
|
private ensureDaemonTask(script: string): void {
|
|
156
142
|
const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
|
|
157
143
|
const tr = `"${process.execPath}" "${script}" serve`;
|
|
@@ -160,7 +146,7 @@ export class WindowsPlatform implements PlatformService {
|
|
|
160
146
|
try {
|
|
161
147
|
const bom = Buffer.from([0xFF, 0xFE]);
|
|
162
148
|
fs.writeFileSync(xmlPath, Buffer.concat([bom, Buffer.from(xml, "utf16le")]));
|
|
163
|
-
// S4U
|
|
149
|
+
// S4U requires elevation — spawn schtasks via RunAs.
|
|
164
150
|
const args = `/create /tn "${tn}" /xml "${xmlPath}" /f`;
|
|
165
151
|
execFileSync("powershell", [
|
|
166
152
|
"-Command", `Start-Process -Verb RunAs -Wait -FilePath schtasks -ArgumentList '${args}'`,
|
|
@@ -174,7 +160,7 @@ export class WindowsPlatform implements PlatformService {
|
|
|
174
160
|
|
|
175
161
|
}
|
|
176
162
|
|
|
177
|
-
/**
|
|
163
|
+
/** Starting via Task Scheduler runs the daemon outside any session's job object. */
|
|
178
164
|
private startDaemonTask(): void {
|
|
179
165
|
const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
|
|
180
166
|
try {
|
|
@@ -192,9 +178,8 @@ export class WindowsPlatform implements PlatformService {
|
|
|
192
178
|
const script = process.argv[1] || "palmier";
|
|
193
179
|
const tr = `"${process.execPath}" "${script}" run ${taskId}`;
|
|
194
180
|
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
// scheduler — they intentionally produce only the dummy trigger below.
|
|
181
|
+
// Event-based schedule types (on_new_notification/on_new_sms) are driven by
|
|
182
|
+
// the run process, not the OS scheduler — they fall through to the dummy trigger.
|
|
198
183
|
const triggerElements: string[] = [];
|
|
199
184
|
const scheduleType = task.frontmatter.schedule_type;
|
|
200
185
|
const scheduleValues = task.frontmatter.schedule_values;
|
|
@@ -208,19 +193,19 @@ export class WindowsPlatform implements PlatformService {
|
|
|
208
193
|
}
|
|
209
194
|
}
|
|
210
195
|
}
|
|
211
|
-
//
|
|
196
|
+
// Dummy trigger so schtasks /run still works.
|
|
212
197
|
if (triggerElements.length === 0) {
|
|
213
198
|
triggerElements.push(`<TimeTrigger><StartBoundary>2000-01-01T00:00:00</StartBoundary></TimeTrigger>`);
|
|
214
199
|
}
|
|
215
200
|
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
201
|
+
// XML registration (vs schtasks flags) gives us access to settings like
|
|
202
|
+
// MultipleInstancesPolicy. S4U keeps the console hidden unless
|
|
203
|
+
// foreground_mode is set. Works unelevated because the caller (daemon)
|
|
204
|
+
// runs elevated.
|
|
220
205
|
const xml = buildTaskXml(tr, triggerElements, task.frontmatter.foreground_mode);
|
|
221
206
|
const xmlPath = path.join(CONFIG_DIR, `task-${taskId}.xml`);
|
|
222
207
|
try {
|
|
223
|
-
// schtasks /xml requires UTF-16LE with BOM
|
|
208
|
+
// schtasks /xml requires UTF-16LE with BOM.
|
|
224
209
|
const bom = Buffer.from([0xFF, 0xFE]);
|
|
225
210
|
fs.writeFileSync(xmlPath, Buffer.concat([bom, Buffer.from(xml, "utf16le")]));
|
|
226
211
|
execFileSync("schtasks", [
|
|
@@ -239,9 +224,7 @@ export class WindowsPlatform implements PlatformService {
|
|
|
239
224
|
const tn = schtasksTaskName(taskId);
|
|
240
225
|
try {
|
|
241
226
|
execFileSync("schtasks", ["/delete", "/tn", tn, "/f"], { encoding: "utf-8", windowsHide: true });
|
|
242
|
-
} catch {
|
|
243
|
-
// Task might not exist — that's fine
|
|
244
|
-
}
|
|
227
|
+
} catch { /* task may not exist */ }
|
|
245
228
|
}
|
|
246
229
|
|
|
247
230
|
async startTask(taskId: string): Promise<void> {
|
|
@@ -255,8 +238,8 @@ export class WindowsPlatform implements PlatformService {
|
|
|
255
238
|
}
|
|
256
239
|
|
|
257
240
|
async stopTask(taskId: string): Promise<void> {
|
|
258
|
-
//
|
|
259
|
-
//
|
|
241
|
+
// schtasks /end leaves agent children orphaned, so kill the process tree
|
|
242
|
+
// via the PID recorded in status.json first.
|
|
260
243
|
try {
|
|
261
244
|
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
262
245
|
const status = readTaskStatus(taskDir);
|
|
@@ -265,10 +248,9 @@ export class WindowsPlatform implements PlatformService {
|
|
|
265
248
|
return;
|
|
266
249
|
}
|
|
267
250
|
} catch {
|
|
268
|
-
// PID may be stale or config unavailable; fall through to schtasks /end
|
|
251
|
+
// PID may be stale or config unavailable; fall through to schtasks /end.
|
|
269
252
|
}
|
|
270
253
|
|
|
271
|
-
// Fallback: schtasks /end (kills top-level process only)
|
|
272
254
|
const tn = schtasksTaskName(taskId);
|
|
273
255
|
try {
|
|
274
256
|
execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true });
|
|
@@ -279,7 +261,6 @@ export class WindowsPlatform implements PlatformService {
|
|
|
279
261
|
}
|
|
280
262
|
|
|
281
263
|
isTaskRunning(taskId: string): boolean {
|
|
282
|
-
// Check Task Scheduler first (for scheduled/on-demand runs)
|
|
283
264
|
const tn = schtasksTaskName(taskId);
|
|
284
265
|
try {
|
|
285
266
|
const out = execFileSync("schtasks", ["/query", "/tn", tn, "/fo", "CSV", "/nh"], {
|
|
@@ -289,18 +270,17 @@ export class WindowsPlatform implements PlatformService {
|
|
|
289
270
|
if (out.includes('"Running"')) return true;
|
|
290
271
|
} catch { /* task may not exist in scheduler */ }
|
|
291
272
|
|
|
292
|
-
//
|
|
273
|
+
// Follow-up runs are spawned directly (not via schtasks), so check PID too.
|
|
293
274
|
try {
|
|
294
275
|
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
295
276
|
const status = readTaskStatus(taskDir);
|
|
296
277
|
if (status?.pid) {
|
|
297
|
-
// tasklist exits 0 if the PID is found
|
|
298
278
|
execFileSync("tasklist", ["/fi", `PID eq ${status.pid}`, "/nh"], {
|
|
299
279
|
encoding: "utf-8",
|
|
300
280
|
windowsHide: true,
|
|
301
281
|
stdio: "pipe",
|
|
302
282
|
});
|
|
303
|
-
// tasklist always exits 0
|
|
283
|
+
// tasklist always exits 0, so match the output for the PID.
|
|
304
284
|
const out = execFileSync("tasklist", ["/fi", `PID eq ${status.pid}`, "/fo", "CSV", "/nh"], {
|
|
305
285
|
encoding: "utf-8",
|
|
306
286
|
windowsHide: true,
|
|
@@ -314,7 +294,6 @@ export class WindowsPlatform implements PlatformService {
|
|
|
314
294
|
}
|
|
315
295
|
|
|
316
296
|
getGuiEnv(): Record<string, string> {
|
|
317
|
-
// Windows GUI is always available — no special env vars needed
|
|
318
297
|
return {};
|
|
319
298
|
}
|
|
320
299
|
}
|
package/src/rpc-handler.ts
CHANGED
|
@@ -17,9 +17,6 @@ import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./comma
|
|
|
17
17
|
import { clearTaskQueue } from "./event-queues.js";
|
|
18
18
|
import type { HostConfig, ParsedTask, RpcMessage, ConversationMessage } from "./types.js";
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Parse RESULT frontmatter and conversation messages.
|
|
22
|
-
*/
|
|
23
20
|
export function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
24
21
|
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
25
22
|
if (!fmMatch) return { messages: [] };
|
|
@@ -33,19 +30,16 @@ export function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
|
33
30
|
|
|
34
31
|
const messages = parseConversationMessages(fmMatch[2]);
|
|
35
32
|
|
|
36
|
-
// Derive state from status messages — just look at the last one
|
|
37
33
|
const statusMessages = messages.filter((m: ConversationMessage) => m.role === "status");
|
|
38
34
|
const lastStatus = statusMessages[statusMessages.length - 1];
|
|
39
35
|
const startedMsg = statusMessages.find((m: ConversationMessage) => m.type === "started");
|
|
40
36
|
const terminalStates = ["finished", "failed", "aborted"];
|
|
41
37
|
const terminalMsg = [...statusMessages].reverse().find((m: ConversationMessage) => terminalStates.includes(m.type ?? ""));
|
|
42
38
|
|
|
43
|
-
// If last status is "started" (or continuation like "confirmation"/"monitoring"),
|
|
44
|
-
// determine if it's a task run or follow-up
|
|
45
39
|
const activeStates = ["started", "monitoring", "confirmation"];
|
|
46
40
|
let runningState: string | undefined;
|
|
47
41
|
if (lastStatus?.type === "monitoring") {
|
|
48
|
-
//
|
|
42
|
+
// Show "monitoring" only if no assistant/user message followed it.
|
|
49
43
|
const lastStatusIdx = messages.lastIndexOf(lastStatus);
|
|
50
44
|
const hasMessageAfter = messages.slice(lastStatusIdx + 1).some((m: ConversationMessage) => m.role === "assistant" || m.role === "user");
|
|
51
45
|
runningState = hasMessageAfter ? "started" : "monitoring";
|
|
@@ -65,16 +59,13 @@ export function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
|
65
59
|
};
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
/**
|
|
69
|
-
* Parse conversation messages from the body of a RESULT file.
|
|
70
|
-
*/
|
|
71
62
|
function parseConversationMessages(body: string): ConversationMessage[] {
|
|
72
63
|
const delimiterRegex = /<!-- palmier:message\s+(.*?)\s*-->/g;
|
|
73
64
|
const messages: ConversationMessage[] = [];
|
|
74
65
|
const matches = [...body.matchAll(delimiterRegex)];
|
|
75
66
|
|
|
76
67
|
if (matches.length === 0) {
|
|
77
|
-
// No delimiters — treat entire body as single assistant message
|
|
68
|
+
// No delimiters — treat entire body as a single assistant message.
|
|
78
69
|
const content = body.trim();
|
|
79
70
|
if (content) {
|
|
80
71
|
messages.push({ role: "assistant", time: 0, content });
|
|
@@ -106,10 +97,6 @@ function parseAttr(attrs: string, name: string): string | undefined {
|
|
|
106
97
|
return match ? match[1] : undefined;
|
|
107
98
|
}
|
|
108
99
|
|
|
109
|
-
/**
|
|
110
|
-
* Generate a concise task name from a user prompt using the given agent.
|
|
111
|
-
* Falls back to the raw prompt on failure.
|
|
112
|
-
*/
|
|
113
100
|
async function generateName(
|
|
114
101
|
projectRoot: string,
|
|
115
102
|
userPrompt: string,
|
|
@@ -136,9 +123,6 @@ async function generateName(
|
|
|
136
123
|
/** Active follow-up child processes, keyed by "taskId:runId". */
|
|
137
124
|
const activeFollowups = new Map<string, ChildProcess>();
|
|
138
125
|
|
|
139
|
-
/**
|
|
140
|
-
* Create a transport-agnostic RPC handler bound to the given config.
|
|
141
|
-
*/
|
|
142
126
|
export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
143
127
|
function flattenTask(task: ParsedTask) {
|
|
144
128
|
const taskDir = getTaskDir(config.projectRoot, task.frontmatter.id);
|
|
@@ -150,8 +134,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
150
134
|
}
|
|
151
135
|
|
|
152
136
|
async function handleRpc(request: RpcMessage): Promise<unknown> {
|
|
153
|
-
//
|
|
154
|
-
//
|
|
137
|
+
// task.user_input comes from server-originated push responses; it's gated
|
|
138
|
+
// by getPending() rather than a client token.
|
|
155
139
|
const skipAuth = request.method === "task.user_input";
|
|
156
140
|
if (!skipAuth && !request.localhost && (!request.clientToken || !validateClient(request.clientToken))) {
|
|
157
141
|
return { error: "Unauthorized" };
|
|
@@ -159,11 +143,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
159
143
|
|
|
160
144
|
switch (request.method) {
|
|
161
145
|
case "host.info": {
|
|
162
|
-
// Bootstrap metadata the PWA needs on connect, independent of which tab
|
|
163
|
-
// is active. Includes any prompts already waiting so a reconnecting
|
|
164
|
-
// PWA can render their modals without replaying events.
|
|
165
146
|
const capabilities: Record<string, string | null> = {};
|
|
166
|
-
for (const capability of ["location", "notifications", "sms", "contacts", "calendar", "
|
|
147
|
+
for (const capability of ["location", "notifications", "sms-read", "sms-send", "contacts", "calendar", "alarm", "battery", "dnd", "send-email"] as const) {
|
|
167
148
|
capabilities[capability] = getCapabilityDevice(capability)?.clientToken ?? null;
|
|
168
149
|
}
|
|
169
150
|
return {
|
|
@@ -250,11 +231,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
250
231
|
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
251
232
|
const existing = parseTaskFile(taskDir);
|
|
252
233
|
|
|
253
|
-
// Detect whether name needs regeneration
|
|
254
234
|
const promptChanged = params.user_prompt !== undefined && params.user_prompt !== existing.frontmatter.user_prompt;
|
|
255
235
|
const agentChanged = params.agent !== undefined && params.agent !== existing.frontmatter.agent;
|
|
256
236
|
|
|
257
|
-
// Merge updates
|
|
258
237
|
if (params.user_prompt !== undefined) existing.frontmatter.user_prompt = params.user_prompt;
|
|
259
238
|
if (params.agent !== undefined) existing.frontmatter.agent = params.agent;
|
|
260
239
|
if (params.schedule_type !== undefined) {
|
|
@@ -287,7 +266,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
287
266
|
}
|
|
288
267
|
}
|
|
289
268
|
|
|
290
|
-
// Regenerate name when prompt or agent changes
|
|
291
269
|
if (promptChanged || agentChanged) {
|
|
292
270
|
existing.frontmatter.name = existing.frontmatter.user_prompt.length <= 50
|
|
293
271
|
? existing.frontmatter.user_prompt
|
|
@@ -296,8 +274,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
296
274
|
|
|
297
275
|
writeTaskFile(taskDir, existing);
|
|
298
276
|
|
|
299
|
-
//
|
|
300
|
-
//
|
|
277
|
+
// installTaskTimer overwrites in-place (schtasks /f, systemd unit rewrite)
|
|
278
|
+
// without killing a running task process.
|
|
301
279
|
getPlatform().installTaskTimer(config, existing);
|
|
302
280
|
|
|
303
281
|
return flattenTask(existing);
|
|
@@ -341,13 +319,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
341
319
|
};
|
|
342
320
|
|
|
343
321
|
writeTaskFile(taskDir, task);
|
|
344
|
-
//
|
|
322
|
+
// One-off run: do NOT append to tasks.jsonl.
|
|
345
323
|
|
|
346
|
-
// Create initial result file so it appears in runs list immediately
|
|
347
324
|
const runId = createRunDir(taskDir, name, Date.now(), params.agent);
|
|
348
325
|
appendHistory(config.projectRoot, { task_id: id, run_id: runId });
|
|
349
326
|
|
|
350
|
-
// Spawn `palmier run <id>` directly as a detached process
|
|
351
327
|
const script = process.argv[1] || "palmier";
|
|
352
328
|
const child = spawn(process.execPath, [script, "run", id], {
|
|
353
329
|
detached: true,
|
|
@@ -365,13 +341,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
365
341
|
const runTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
366
342
|
const platform = getPlatform();
|
|
367
343
|
|
|
368
|
-
// If the task is already running, kill the stale process and start fresh
|
|
369
344
|
if (platform.isTaskRunning(params.id)) {
|
|
370
345
|
console.log(`[task.run] Task ${params.id} is already running, killing stale process`);
|
|
371
346
|
await platform.stopTask(params.id);
|
|
372
347
|
}
|
|
373
348
|
|
|
374
|
-
// Create initial result file so it appears in runs list immediately
|
|
375
349
|
const runTask = parseTaskFile(runTaskDir);
|
|
376
350
|
const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now(), runTask.frontmatter.agent);
|
|
377
351
|
appendHistory(config.projectRoot, { task_id: params.id, run_id: taskRunId });
|
|
@@ -399,7 +373,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
399
373
|
const followupTask = parseTaskFile(followupTaskDir);
|
|
400
374
|
const followupRunDir = getRunDir(followupTaskDir, params.run_id);
|
|
401
375
|
|
|
402
|
-
// Append user message + started status
|
|
403
376
|
appendRunMessage(followupTaskDir, params.run_id, {
|
|
404
377
|
role: "user",
|
|
405
378
|
time: Date.now(),
|
|
@@ -413,13 +386,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
413
386
|
});
|
|
414
387
|
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
415
388
|
|
|
416
|
-
// Fire-and-forget: invoke agent inline as a child of the serve process
|
|
417
389
|
const followupAgent = getAgent(followupTask.frontmatter.agent);
|
|
418
390
|
const { command: cmd, args: cmdArgs, stdin, env: followupAgentEnv } = followupAgent.getTaskRunCommandLine(
|
|
419
391
|
followupTask, params.message, followupTask.frontmatter.yolo_mode ? "yolo" : followupTask.frontmatter.permissions,
|
|
420
392
|
);
|
|
421
393
|
|
|
422
|
-
// Spawn directly via crossSpawn so we can track and kill the child
|
|
423
394
|
const child = crossSpawn(cmd, cmdArgs, {
|
|
424
395
|
cwd: followupRunDir,
|
|
425
396
|
stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
@@ -429,14 +400,13 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
429
400
|
if (stdin != null) child.stdin!.end(stdin);
|
|
430
401
|
activeFollowups.set(followupKey, child);
|
|
431
402
|
|
|
432
|
-
// Collect output
|
|
433
403
|
const chunks: Buffer[] = [];
|
|
434
404
|
child.stdout?.on("data", (d: Buffer) => chunks.push(d));
|
|
435
405
|
child.stderr?.on("data", (d: Buffer) => process.stderr.write(d));
|
|
436
406
|
|
|
437
407
|
child.on("close", async (code: number | null) => {
|
|
438
408
|
activeFollowups.delete(followupKey);
|
|
439
|
-
//
|
|
409
|
+
// stop_followup already wrote the stopped status.
|
|
440
410
|
if (child.killed) return;
|
|
441
411
|
|
|
442
412
|
const output = Buffer.concat(chunks).toString("utf-8");
|
|
@@ -484,7 +454,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
484
454
|
return { error: "No active follow-up for this run" };
|
|
485
455
|
}
|
|
486
456
|
|
|
487
|
-
// Kill the child process tree
|
|
488
457
|
if (process.platform === "win32" && child.pid) {
|
|
489
458
|
try {
|
|
490
459
|
const { execFileSync } = await import("child_process");
|
|
@@ -494,7 +463,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
494
463
|
child.kill();
|
|
495
464
|
}
|
|
496
465
|
|
|
497
|
-
//
|
|
466
|
+
// child.killed stops the close handler from double-writing the status.
|
|
498
467
|
const stopTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
499
468
|
appendRunMessage(stopTaskDir, params.run_id, {
|
|
500
469
|
role: "status",
|
|
@@ -510,17 +479,16 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
510
479
|
case "task.abort": {
|
|
511
480
|
const params = request.params as { id: string };
|
|
512
481
|
const abortTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
513
|
-
// Read
|
|
514
|
-
//
|
|
482
|
+
// Read PID before overwriting — stopTask needs it to kill the
|
|
483
|
+
// process tree on Windows.
|
|
515
484
|
const abortPrevStatus = readTaskStatus(abortTaskDir);
|
|
516
|
-
// Write abort status
|
|
517
|
-
// handler
|
|
485
|
+
// Write abort status before killing so the dying process's signal
|
|
486
|
+
// handler sees this was RPC-initiated and skips publishing.
|
|
518
487
|
writeTaskStatus(abortTaskDir, {
|
|
519
488
|
running_state: "aborted",
|
|
520
489
|
time_stamp: Date.now(),
|
|
521
490
|
...(abortPrevStatus?.pid ? { pid: abortPrevStatus.pid } : {}),
|
|
522
491
|
});
|
|
523
|
-
// Append aborted status to the latest run
|
|
524
492
|
try {
|
|
525
493
|
const runDirs = fs.readdirSync(abortTaskDir)
|
|
526
494
|
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(abortTaskDir, f, "TASKRUN.md")))
|
|
@@ -543,7 +511,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
543
511
|
console.error(`task.abort failed for ${params.id}: ${e.stderr || e.message}`);
|
|
544
512
|
return { error: `Failed to abort task: ${e.stderr || e.message}` };
|
|
545
513
|
}
|
|
546
|
-
// Notify connected clients (NATS + HTTP SSE if LAN server is running)
|
|
547
514
|
const abortPayload: Record<string, unknown> = { event_type: "running-state", running_state: "aborted" };
|
|
548
515
|
await publishHostEvent(nc, config.hostId, params.id, abortPayload);
|
|
549
516
|
return { ok: true, task_id: params.id };
|
package/src/spawn-command.ts
CHANGED
|
@@ -18,19 +18,12 @@ export interface SpawnStreamingOptions {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Spawn
|
|
22
|
-
* with stdout piped
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* shell: true is required so users can write piped commands like
|
|
28
|
-
* "tail -f log | grep ERROR".
|
|
29
|
-
*
|
|
30
|
-
* stdin is "pipe" (kept open, never written to) rather than "ignore"
|
|
31
|
-
* (/dev/null). Some long-running commands exit when stdin is closed/EOF.
|
|
32
|
-
* This differs from spawnCommand() which uses "ignore" because agent
|
|
33
|
-
* CLIs like `claude -p` hang on an open stdin pipe.
|
|
21
|
+
* Spawn with shell interpretation for piped commands like "tail -f log | grep".
|
|
22
|
+
* Returns the ChildProcess with stdout piped so the caller can read it directly
|
|
23
|
+
* (contrast with spawnCommand which buffers). stdin is "pipe" (held open, not
|
|
24
|
+
* written to): some long-running commands exit on stdin EOF. Agent CLIs like
|
|
25
|
+
* `claude -p` conversely hang on an open stdin, which is why spawnCommand
|
|
26
|
+
* defaults to "ignore".
|
|
34
27
|
*/
|
|
35
28
|
export function spawnStreamingCommand(
|
|
36
29
|
command: string,
|
|
@@ -60,18 +53,10 @@ export interface SpawnCommandOptions {
|
|
|
60
53
|
}
|
|
61
54
|
|
|
62
55
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* On other platforms the command is executed directly (no shell), so no
|
|
69
|
-
* escaping is needed.
|
|
70
|
-
*
|
|
71
|
-
* stdin is set to "ignore" by default (equivalent to < /dev/null) because
|
|
72
|
-
* tools like `claude -p` hang indefinitely on an open stdin pipe.
|
|
73
|
-
* When opts.stdin is provided, stdin is set to "pipe" and the string is
|
|
74
|
-
* written to the process before closing the pipe.
|
|
56
|
+
* cross-spawn resolves .cmd shims and escapes args on Windows without shell:true
|
|
57
|
+
* (which mishandles special characters). stdin defaults to "ignore" because
|
|
58
|
+
* tools like `claude -p` hang on an open stdin pipe; pass opts.stdin to write
|
|
59
|
+
* a string and then close the pipe.
|
|
75
60
|
*/
|
|
76
61
|
export interface SpawnCommandResult {
|
|
77
62
|
output: string;
|
|
@@ -84,8 +69,7 @@ export function spawnCommand(
|
|
|
84
69
|
opts: SpawnCommandOptions,
|
|
85
70
|
): Promise<SpawnCommandResult> {
|
|
86
71
|
return new Promise<SpawnCommandResult>((resolve, reject) => {
|
|
87
|
-
//
|
|
88
|
-
// in arguments, and CLI prompts don't need them.
|
|
72
|
+
// cmd.exe can't handle literal newlines in arguments.
|
|
89
73
|
const finalArgs = process.platform === "win32"
|
|
90
74
|
? args.map((a) => a.replace(/[\r\n]+/g, " "))
|
|
91
75
|
: args;
|