palmier 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +13 -0
- package/README.md +11 -11
- package/dist/agents/agent.d.ts +0 -4
- package/dist/agents/claude.js +1 -1
- package/dist/agents/codex.js +2 -2
- package/dist/agents/cursor.js +1 -1
- package/dist/agents/deepagents.js +1 -1
- package/dist/agents/gemini.js +3 -2
- package/dist/agents/goose.js +1 -1
- package/dist/agents/hermes.js +1 -1
- package/dist/agents/kiro.js +1 -1
- package/dist/agents/opencode.js +1 -1
- package/dist/agents/qoder.js +1 -1
- package/dist/agents/shared-prompt.d.ts +0 -3
- package/dist/agents/shared-prompt.js +0 -3
- package/dist/app-registry.d.ts +10 -0
- package/dist/app-registry.js +44 -0
- package/dist/commands/info.d.ts +0 -3
- package/dist/commands/info.js +0 -5
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.js +2 -11
- package/dist/commands/pair.d.ts +1 -4
- package/dist/commands/pair.js +1 -12
- package/dist/commands/restart.d.ts +0 -3
- package/dist/commands/restart.js +0 -3
- package/dist/commands/run.d.ts +1 -14
- package/dist/commands/run.js +18 -61
- package/dist/commands/serve.d.ts +0 -3
- package/dist/commands/serve.js +33 -27
- package/dist/config.d.ts +0 -8
- package/dist/config.js +0 -8
- package/dist/device-capabilities.d.ts +1 -1
- package/dist/event-queues.d.ts +6 -21
- package/dist/event-queues.js +6 -21
- package/dist/events.d.ts +0 -6
- package/dist/events.js +1 -9
- package/dist/index.js +0 -1
- package/dist/mcp-handler.js +1 -2
- package/dist/mcp-tools.d.ts +0 -3
- package/dist/mcp-tools.js +12 -16
- package/dist/nats-client.d.ts +0 -3
- package/dist/nats-client.js +1 -4
- package/dist/pending-requests.d.ts +4 -18
- package/dist/pending-requests.js +4 -18
- package/dist/platform/index.d.ts +1 -4
- package/dist/platform/index.js +1 -4
- package/dist/platform/linux.d.ts +3 -9
- package/dist/platform/linux.js +9 -20
- package/dist/platform/platform.d.ts +1 -4
- package/dist/platform/windows.d.ts +2 -5
- package/dist/platform/windows.js +19 -39
- package/dist/pwa/assets/{index-CQxcuDhM.css → index-B0F9mtid.css} +1 -1
- package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
- package/dist/pwa/assets/{web-D7Kq3Nvk.js → web-C6lkQj9J.js} +1 -1
- package/dist/pwa/assets/{web-DOyOiwsW.js → web-Z1623me-.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.d.ts +0 -6
- package/dist/rpc-handler.js +18 -47
- package/dist/spawn-command.d.ts +10 -25
- package/dist/spawn-command.js +7 -15
- package/dist/task.d.ts +6 -64
- package/dist/task.js +7 -70
- package/dist/transports/http-transport.d.ts +0 -4
- package/dist/transports/http-transport.js +6 -28
- package/dist/transports/nats-transport.d.ts +0 -4
- package/dist/transports/nats-transport.js +3 -9
- package/dist/types.d.ts +3 -7
- package/dist/update-checker.d.ts +1 -4
- package/dist/update-checker.js +2 -5
- package/package.json +1 -1
- package/palmier-server/pwa/src/App.css +165 -20
- package/palmier-server/pwa/src/components/HostMenu.tsx +159 -49
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
- package/palmier-server/pwa/src/components/SessionsView.tsx +57 -31
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
- package/palmier-server/pwa/src/components/TaskForm.tsx +152 -2
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +20 -2
- package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
- package/palmier-server/server/src/index.ts +7 -7
- package/palmier-server/server/src/routes/device.ts +4 -4
- package/palmier-server/spec.md +38 -7
- package/src/agents/agent.ts +0 -4
- package/src/agents/claude.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/cursor.ts +1 -1
- package/src/agents/deepagents.ts +1 -1
- package/src/agents/gemini.ts +3 -2
- package/src/agents/goose.ts +1 -1
- package/src/agents/hermes.ts +1 -1
- package/src/agents/kiro.ts +1 -1
- package/src/agents/opencode.ts +1 -1
- package/src/agents/qoder.ts +1 -1
- package/src/agents/shared-prompt.ts +0 -3
- package/src/app-registry.ts +52 -0
- package/src/commands/info.ts +0 -5
- package/src/commands/init.ts +2 -11
- package/src/commands/pair.ts +1 -12
- package/src/commands/restart.ts +0 -3
- package/src/commands/run.ts +18 -65
- package/src/commands/serve.ts +31 -27
- package/src/config.ts +0 -8
- package/src/device-capabilities.ts +3 -2
- package/src/event-queues.ts +6 -21
- package/src/events.ts +1 -9
- package/src/index.ts +0 -1
- package/src/mcp-handler.ts +1 -2
- package/src/mcp-tools.ts +12 -18
- package/src/nats-client.ts +1 -4
- package/src/pending-requests.ts +4 -18
- package/src/platform/index.ts +1 -4
- package/src/platform/linux.ts +9 -20
- package/src/platform/platform.ts +1 -4
- package/src/platform/windows.ts +19 -40
- package/src/rpc-handler.ts +19 -47
- package/src/spawn-command.ts +11 -27
- package/src/task.ts +7 -70
- package/src/transports/http-transport.ts +6 -39
- package/src/transports/nats-transport.ts +3 -9
- package/src/types.ts +3 -10
- package/src/update-checker.ts +2 -5
- package/test/task-parsing.test.ts +2 -3
- package/test/windows-xml.test.ts +11 -12
- package/dist/pwa/assets/index-DQfOEB03.js +0 -120
package/src/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
|
@@ -12,14 +12,12 @@ import { getAgent } from "./agents/agent.js";
|
|
|
12
12
|
import { validateClient } from "./client-store.js";
|
|
13
13
|
import { publishHostEvent } from "./events.js";
|
|
14
14
|
import { getCapabilityDevice, setCapabilityDevice, clearCapabilityDevice, type DeviceCapability } from "./device-capabilities.js";
|
|
15
|
+
import { listApps } from "./app-registry.js";
|
|
15
16
|
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
16
17
|
import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
|
|
17
18
|
import { clearTaskQueue } from "./event-queues.js";
|
|
18
19
|
import type { HostConfig, ParsedTask, RpcMessage, ConversationMessage } from "./types.js";
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
-
* Parse RESULT frontmatter and conversation messages.
|
|
22
|
-
*/
|
|
23
21
|
export function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
24
22
|
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
25
23
|
if (!fmMatch) return { messages: [] };
|
|
@@ -33,19 +31,16 @@ export function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
|
33
31
|
|
|
34
32
|
const messages = parseConversationMessages(fmMatch[2]);
|
|
35
33
|
|
|
36
|
-
// Derive state from status messages — just look at the last one
|
|
37
34
|
const statusMessages = messages.filter((m: ConversationMessage) => m.role === "status");
|
|
38
35
|
const lastStatus = statusMessages[statusMessages.length - 1];
|
|
39
36
|
const startedMsg = statusMessages.find((m: ConversationMessage) => m.type === "started");
|
|
40
37
|
const terminalStates = ["finished", "failed", "aborted"];
|
|
41
38
|
const terminalMsg = [...statusMessages].reverse().find((m: ConversationMessage) => terminalStates.includes(m.type ?? ""));
|
|
42
39
|
|
|
43
|
-
// If last status is "started" (or continuation like "confirmation"/"monitoring"),
|
|
44
|
-
// determine if it's a task run or follow-up
|
|
45
40
|
const activeStates = ["started", "monitoring", "confirmation"];
|
|
46
41
|
let runningState: string | undefined;
|
|
47
42
|
if (lastStatus?.type === "monitoring") {
|
|
48
|
-
//
|
|
43
|
+
// Show "monitoring" only if no assistant/user message followed it.
|
|
49
44
|
const lastStatusIdx = messages.lastIndexOf(lastStatus);
|
|
50
45
|
const hasMessageAfter = messages.slice(lastStatusIdx + 1).some((m: ConversationMessage) => m.role === "assistant" || m.role === "user");
|
|
51
46
|
runningState = hasMessageAfter ? "started" : "monitoring";
|
|
@@ -65,16 +60,13 @@ export function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
|
65
60
|
};
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
/**
|
|
69
|
-
* Parse conversation messages from the body of a RESULT file.
|
|
70
|
-
*/
|
|
71
63
|
function parseConversationMessages(body: string): ConversationMessage[] {
|
|
72
64
|
const delimiterRegex = /<!-- palmier:message\s+(.*?)\s*-->/g;
|
|
73
65
|
const messages: ConversationMessage[] = [];
|
|
74
66
|
const matches = [...body.matchAll(delimiterRegex)];
|
|
75
67
|
|
|
76
68
|
if (matches.length === 0) {
|
|
77
|
-
// No delimiters — treat entire body as single assistant message
|
|
69
|
+
// No delimiters — treat entire body as a single assistant message.
|
|
78
70
|
const content = body.trim();
|
|
79
71
|
if (content) {
|
|
80
72
|
messages.push({ role: "assistant", time: 0, content });
|
|
@@ -106,10 +98,6 @@ function parseAttr(attrs: string, name: string): string | undefined {
|
|
|
106
98
|
return match ? match[1] : undefined;
|
|
107
99
|
}
|
|
108
100
|
|
|
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
101
|
async function generateName(
|
|
114
102
|
projectRoot: string,
|
|
115
103
|
userPrompt: string,
|
|
@@ -136,9 +124,6 @@ async function generateName(
|
|
|
136
124
|
/** Active follow-up child processes, keyed by "taskId:runId". */
|
|
137
125
|
const activeFollowups = new Map<string, ChildProcess>();
|
|
138
126
|
|
|
139
|
-
/**
|
|
140
|
-
* Create a transport-agnostic RPC handler bound to the given config.
|
|
141
|
-
*/
|
|
142
127
|
export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
143
128
|
function flattenTask(task: ParsedTask) {
|
|
144
129
|
const taskDir = getTaskDir(config.projectRoot, task.frontmatter.id);
|
|
@@ -150,8 +135,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
150
135
|
}
|
|
151
136
|
|
|
152
137
|
async function handleRpc(request: RpcMessage): Promise<unknown> {
|
|
153
|
-
//
|
|
154
|
-
//
|
|
138
|
+
// task.user_input comes from server-originated push responses; it's gated
|
|
139
|
+
// by getPending() rather than a client token.
|
|
155
140
|
const skipAuth = request.method === "task.user_input";
|
|
156
141
|
if (!skipAuth && !request.localhost && (!request.clientToken || !validateClient(request.clientToken))) {
|
|
157
142
|
return { error: "Unauthorized" };
|
|
@@ -159,11 +144,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
159
144
|
|
|
160
145
|
switch (request.method) {
|
|
161
146
|
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
147
|
const capabilities: Record<string, string | null> = {};
|
|
166
|
-
for (const capability of ["location", "notifications", "sms", "contacts", "calendar", "
|
|
148
|
+
for (const capability of ["location", "notifications", "sms-read", "sms-send", "contacts", "calendar", "alarm", "battery", "dnd", "send-email"] as const) {
|
|
167
149
|
capabilities[capability] = getCapabilityDevice(capability)?.clientToken ?? null;
|
|
168
150
|
}
|
|
169
151
|
return {
|
|
@@ -250,11 +232,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
250
232
|
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
251
233
|
const existing = parseTaskFile(taskDir);
|
|
252
234
|
|
|
253
|
-
// Detect whether name needs regeneration
|
|
254
235
|
const promptChanged = params.user_prompt !== undefined && params.user_prompt !== existing.frontmatter.user_prompt;
|
|
255
236
|
const agentChanged = params.agent !== undefined && params.agent !== existing.frontmatter.agent;
|
|
256
237
|
|
|
257
|
-
// Merge updates
|
|
258
238
|
if (params.user_prompt !== undefined) existing.frontmatter.user_prompt = params.user_prompt;
|
|
259
239
|
if (params.agent !== undefined) existing.frontmatter.agent = params.agent;
|
|
260
240
|
if (params.schedule_type !== undefined) {
|
|
@@ -287,7 +267,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
287
267
|
}
|
|
288
268
|
}
|
|
289
269
|
|
|
290
|
-
// Regenerate name when prompt or agent changes
|
|
291
270
|
if (promptChanged || agentChanged) {
|
|
292
271
|
existing.frontmatter.name = existing.frontmatter.user_prompt.length <= 50
|
|
293
272
|
? existing.frontmatter.user_prompt
|
|
@@ -296,8 +275,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
296
275
|
|
|
297
276
|
writeTaskFile(taskDir, existing);
|
|
298
277
|
|
|
299
|
-
//
|
|
300
|
-
//
|
|
278
|
+
// installTaskTimer overwrites in-place (schtasks /f, systemd unit rewrite)
|
|
279
|
+
// without killing a running task process.
|
|
301
280
|
getPlatform().installTaskTimer(config, existing);
|
|
302
281
|
|
|
303
282
|
return flattenTask(existing);
|
|
@@ -341,13 +320,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
341
320
|
};
|
|
342
321
|
|
|
343
322
|
writeTaskFile(taskDir, task);
|
|
344
|
-
//
|
|
323
|
+
// One-off run: do NOT append to tasks.jsonl.
|
|
345
324
|
|
|
346
|
-
// Create initial result file so it appears in runs list immediately
|
|
347
325
|
const runId = createRunDir(taskDir, name, Date.now(), params.agent);
|
|
348
326
|
appendHistory(config.projectRoot, { task_id: id, run_id: runId });
|
|
349
327
|
|
|
350
|
-
// Spawn `palmier run <id>` directly as a detached process
|
|
351
328
|
const script = process.argv[1] || "palmier";
|
|
352
329
|
const child = spawn(process.execPath, [script, "run", id], {
|
|
353
330
|
detached: true,
|
|
@@ -365,13 +342,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
365
342
|
const runTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
366
343
|
const platform = getPlatform();
|
|
367
344
|
|
|
368
|
-
// If the task is already running, kill the stale process and start fresh
|
|
369
345
|
if (platform.isTaskRunning(params.id)) {
|
|
370
346
|
console.log(`[task.run] Task ${params.id} is already running, killing stale process`);
|
|
371
347
|
await platform.stopTask(params.id);
|
|
372
348
|
}
|
|
373
349
|
|
|
374
|
-
// Create initial result file so it appears in runs list immediately
|
|
375
350
|
const runTask = parseTaskFile(runTaskDir);
|
|
376
351
|
const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now(), runTask.frontmatter.agent);
|
|
377
352
|
appendHistory(config.projectRoot, { task_id: params.id, run_id: taskRunId });
|
|
@@ -399,7 +374,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
399
374
|
const followupTask = parseTaskFile(followupTaskDir);
|
|
400
375
|
const followupRunDir = getRunDir(followupTaskDir, params.run_id);
|
|
401
376
|
|
|
402
|
-
// Append user message + started status
|
|
403
377
|
appendRunMessage(followupTaskDir, params.run_id, {
|
|
404
378
|
role: "user",
|
|
405
379
|
time: Date.now(),
|
|
@@ -413,13 +387,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
413
387
|
});
|
|
414
388
|
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
415
389
|
|
|
416
|
-
// Fire-and-forget: invoke agent inline as a child of the serve process
|
|
417
390
|
const followupAgent = getAgent(followupTask.frontmatter.agent);
|
|
418
391
|
const { command: cmd, args: cmdArgs, stdin, env: followupAgentEnv } = followupAgent.getTaskRunCommandLine(
|
|
419
392
|
followupTask, params.message, followupTask.frontmatter.yolo_mode ? "yolo" : followupTask.frontmatter.permissions,
|
|
420
393
|
);
|
|
421
394
|
|
|
422
|
-
// Spawn directly via crossSpawn so we can track and kill the child
|
|
423
395
|
const child = crossSpawn(cmd, cmdArgs, {
|
|
424
396
|
cwd: followupRunDir,
|
|
425
397
|
stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
@@ -429,14 +401,13 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
429
401
|
if (stdin != null) child.stdin!.end(stdin);
|
|
430
402
|
activeFollowups.set(followupKey, child);
|
|
431
403
|
|
|
432
|
-
// Collect output
|
|
433
404
|
const chunks: Buffer[] = [];
|
|
434
405
|
child.stdout?.on("data", (d: Buffer) => chunks.push(d));
|
|
435
406
|
child.stderr?.on("data", (d: Buffer) => process.stderr.write(d));
|
|
436
407
|
|
|
437
408
|
child.on("close", async (code: number | null) => {
|
|
438
409
|
activeFollowups.delete(followupKey);
|
|
439
|
-
//
|
|
410
|
+
// stop_followup already wrote the stopped status.
|
|
440
411
|
if (child.killed) return;
|
|
441
412
|
|
|
442
413
|
const output = Buffer.concat(chunks).toString("utf-8");
|
|
@@ -484,7 +455,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
484
455
|
return { error: "No active follow-up for this run" };
|
|
485
456
|
}
|
|
486
457
|
|
|
487
|
-
// Kill the child process tree
|
|
488
458
|
if (process.platform === "win32" && child.pid) {
|
|
489
459
|
try {
|
|
490
460
|
const { execFileSync } = await import("child_process");
|
|
@@ -494,7 +464,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
494
464
|
child.kill();
|
|
495
465
|
}
|
|
496
466
|
|
|
497
|
-
//
|
|
467
|
+
// child.killed stops the close handler from double-writing the status.
|
|
498
468
|
const stopTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
499
469
|
appendRunMessage(stopTaskDir, params.run_id, {
|
|
500
470
|
role: "status",
|
|
@@ -510,17 +480,16 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
510
480
|
case "task.abort": {
|
|
511
481
|
const params = request.params as { id: string };
|
|
512
482
|
const abortTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
513
|
-
// Read
|
|
514
|
-
//
|
|
483
|
+
// Read PID before overwriting — stopTask needs it to kill the
|
|
484
|
+
// process tree on Windows.
|
|
515
485
|
const abortPrevStatus = readTaskStatus(abortTaskDir);
|
|
516
|
-
// Write abort status
|
|
517
|
-
// handler
|
|
486
|
+
// Write abort status before killing so the dying process's signal
|
|
487
|
+
// handler sees this was RPC-initiated and skips publishing.
|
|
518
488
|
writeTaskStatus(abortTaskDir, {
|
|
519
489
|
running_state: "aborted",
|
|
520
490
|
time_stamp: Date.now(),
|
|
521
491
|
...(abortPrevStatus?.pid ? { pid: abortPrevStatus.pid } : {}),
|
|
522
492
|
});
|
|
523
|
-
// Append aborted status to the latest run
|
|
524
493
|
try {
|
|
525
494
|
const runDirs = fs.readdirSync(abortTaskDir)
|
|
526
495
|
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(abortTaskDir, f, "TASKRUN.md")))
|
|
@@ -543,7 +512,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
543
512
|
console.error(`task.abort failed for ${params.id}: ${e.stderr || e.message}`);
|
|
544
513
|
return { error: `Failed to abort task: ${e.stderr || e.message}` };
|
|
545
514
|
}
|
|
546
|
-
// Notify connected clients (NATS + HTTP SSE if LAN server is running)
|
|
547
515
|
const abortPayload: Record<string, unknown> = { event_type: "running-state", running_state: "aborted" };
|
|
548
516
|
await publishHostEvent(nc, config.hostId, params.id, abortPayload);
|
|
549
517
|
return { ok: true, task_id: params.id };
|
|
@@ -694,6 +662,10 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
694
662
|
return { ok: true };
|
|
695
663
|
}
|
|
696
664
|
|
|
665
|
+
case "device.notifications.apps": {
|
|
666
|
+
return { apps: listApps() };
|
|
667
|
+
}
|
|
668
|
+
|
|
697
669
|
default:
|
|
698
670
|
return { error: `Unknown method: ${request.method}` };
|
|
699
671
|
}
|
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;
|