palmier 0.2.4 → 0.2.5
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 +12 -16
- package/dist/agents/claude.js +5 -2
- package/dist/agents/codex.js +4 -2
- package/dist/agents/gemini.js +4 -2
- package/dist/commands/info.js +17 -10
- package/dist/commands/init.d.ts +2 -10
- package/dist/commands/init.js +102 -154
- package/dist/commands/run.js +2 -13
- package/dist/commands/sessions.js +1 -4
- package/dist/config.js +1 -1
- package/dist/index.js +7 -7
- package/dist/platform/index.d.ts +4 -0
- package/dist/platform/index.js +12 -0
- package/dist/platform/linux.d.ts +11 -0
- package/dist/platform/linux.js +186 -0
- package/dist/platform/platform.d.ts +20 -0
- package/dist/platform/platform.js +2 -0
- package/dist/platform/windows.d.ts +11 -0
- package/dist/platform/windows.js +201 -0
- package/dist/rpc-handler.js +13 -30
- package/dist/spawn-command.d.ts +5 -2
- package/dist/spawn-command.js +2 -0
- package/dist/transports/http-transport.js +2 -7
- package/package.json +1 -1
- package/src/agents/claude.ts +6 -2
- package/src/agents/codex.ts +5 -2
- package/src/agents/gemini.ts +5 -2
- package/src/commands/info.ts +18 -10
- package/src/commands/init.ts +131 -180
- package/src/commands/run.ts +3 -15
- package/src/commands/sessions.ts +1 -4
- package/src/config.ts +1 -1
- package/src/index.ts +8 -7
- package/src/platform/index.ts +16 -0
- package/src/platform/linux.ts +207 -0
- package/src/platform/platform.ts +25 -0
- package/src/platform/windows.ts +223 -0
- package/src/rpc-handler.ts +13 -37
- package/src/spawn-command.ts +7 -2
- package/src/transports/http-transport.ts +2 -5
- package/src/systemd.ts +0 -164
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { execSync, exec } from "child_process";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
const UNIT_DIR = path.join(homedir(), ".config", "systemd", "user");
|
|
8
|
+
function getTimerName(taskId) {
|
|
9
|
+
return `palmier-task-${taskId}.timer`;
|
|
10
|
+
}
|
|
11
|
+
function getServiceName(taskId) {
|
|
12
|
+
return `palmier-task-${taskId}.service`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Convert a cron expression to a systemd OnCalendar string.
|
|
16
|
+
*
|
|
17
|
+
* Only the 4 cron patterns the PWA UI can produce are supported:
|
|
18
|
+
* hourly: "0 * * * *"
|
|
19
|
+
* daily: "MM HH * * *"
|
|
20
|
+
* weekly: "MM HH * * D"
|
|
21
|
+
* monthly: "MM HH D * *"
|
|
22
|
+
* Arbitrary cron expressions (ranges, lists, steps beyond hourly) are NOT
|
|
23
|
+
* handled because the UI never generates them.
|
|
24
|
+
*/
|
|
25
|
+
function cronToOnCalendar(cron) {
|
|
26
|
+
const parts = cron.trim().split(/\s+/);
|
|
27
|
+
if (parts.length !== 5) {
|
|
28
|
+
throw new Error(`Invalid cron expression (expected 5 fields): ${cron}`);
|
|
29
|
+
}
|
|
30
|
+
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
|
|
31
|
+
// Map cron day-of-week numbers to systemd abbreviated names
|
|
32
|
+
const dowMap = {
|
|
33
|
+
"0": "Sun", "1": "Mon", "2": "Tue", "3": "Wed",
|
|
34
|
+
"4": "Thu", "5": "Fri", "6": "Sat", "7": "Sun",
|
|
35
|
+
};
|
|
36
|
+
const monthPart = "*";
|
|
37
|
+
const dayPart = dayOfMonth === "*" ? "*" : dayOfMonth.padStart(2, "0");
|
|
38
|
+
const hourPart = hour === "*" ? "*" : hour.padStart(2, "0");
|
|
39
|
+
const minutePart = minute === "*" ? "*" : minute.padStart(2, "0");
|
|
40
|
+
if (dayOfWeek !== "*") {
|
|
41
|
+
const dow = dowMap[dayOfWeek] ?? dayOfWeek;
|
|
42
|
+
return `${dow} *-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
|
|
43
|
+
}
|
|
44
|
+
return `*-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
|
|
45
|
+
}
|
|
46
|
+
function daemonReload() {
|
|
47
|
+
try {
|
|
48
|
+
execSync("systemctl --user daemon-reload", { encoding: "utf-8" });
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const e = err;
|
|
52
|
+
console.error(`daemon-reload failed: ${e.stderr || err}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class LinuxPlatform {
|
|
56
|
+
installDaemon(config) {
|
|
57
|
+
fs.mkdirSync(UNIT_DIR, { recursive: true });
|
|
58
|
+
const palmierBin = process.argv[1] || "palmier";
|
|
59
|
+
const serviceContent = `[Unit]
|
|
60
|
+
Description=Palmier Host
|
|
61
|
+
After=network-online.target
|
|
62
|
+
Wants=network-online.target
|
|
63
|
+
|
|
64
|
+
[Service]
|
|
65
|
+
Type=simple
|
|
66
|
+
ExecStart=${palmierBin} serve
|
|
67
|
+
WorkingDirectory=${config.projectRoot}
|
|
68
|
+
Restart=on-failure
|
|
69
|
+
RestartSec=5
|
|
70
|
+
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
71
|
+
|
|
72
|
+
[Install]
|
|
73
|
+
WantedBy=default.target
|
|
74
|
+
`;
|
|
75
|
+
const servicePath = path.join(UNIT_DIR, "palmier.service");
|
|
76
|
+
fs.writeFileSync(servicePath, serviceContent, "utf-8");
|
|
77
|
+
console.log("Systemd service installed at:", servicePath);
|
|
78
|
+
try {
|
|
79
|
+
execSync("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
80
|
+
execSync("systemctl --user enable palmier.service", { stdio: "inherit" });
|
|
81
|
+
execSync("systemctl --user restart palmier.service", { stdio: "inherit" });
|
|
82
|
+
console.log("Palmier host service enabled and started.");
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error(`Warning: failed to enable systemd service: ${err}`);
|
|
86
|
+
console.error("You may need to start it manually: systemctl --user enable --now palmier.service");
|
|
87
|
+
}
|
|
88
|
+
// Enable lingering so service runs without active login session
|
|
89
|
+
try {
|
|
90
|
+
execSync(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
|
|
91
|
+
console.log("Login lingering enabled.");
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
console.error(`Warning: failed to enable linger: ${err}`);
|
|
95
|
+
}
|
|
96
|
+
console.log("\nHost initialization complete!");
|
|
97
|
+
}
|
|
98
|
+
installTaskTimer(config, task) {
|
|
99
|
+
fs.mkdirSync(UNIT_DIR, { recursive: true });
|
|
100
|
+
const taskId = task.frontmatter.id;
|
|
101
|
+
const serviceName = getServiceName(taskId);
|
|
102
|
+
const timerName = getTimerName(taskId);
|
|
103
|
+
const palmierBin = process.argv[1] || "palmier";
|
|
104
|
+
const serviceContent = `[Unit]
|
|
105
|
+
Description=Palmier Task: ${taskId}
|
|
106
|
+
|
|
107
|
+
[Service]
|
|
108
|
+
Type=oneshot
|
|
109
|
+
ExecStart=${palmierBin} run ${taskId}
|
|
110
|
+
WorkingDirectory=${config.projectRoot}
|
|
111
|
+
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
112
|
+
`;
|
|
113
|
+
fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
|
|
114
|
+
daemonReload();
|
|
115
|
+
// Only create and enable a timer if there are actual triggers
|
|
116
|
+
const triggers = task.frontmatter.triggers || [];
|
|
117
|
+
const onCalendarLines = [];
|
|
118
|
+
for (const trigger of triggers) {
|
|
119
|
+
if (trigger.type === "cron") {
|
|
120
|
+
onCalendarLines.push(`OnCalendar=${cronToOnCalendar(trigger.value)}`);
|
|
121
|
+
}
|
|
122
|
+
else if (trigger.type === "once") {
|
|
123
|
+
onCalendarLines.push(`OnActiveSec=${trigger.value}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (onCalendarLines.length > 0) {
|
|
127
|
+
const timerContent = `[Unit]
|
|
128
|
+
Description=Timer for Palmier Task: ${taskId}
|
|
129
|
+
|
|
130
|
+
[Timer]
|
|
131
|
+
${onCalendarLines.join("\n")}
|
|
132
|
+
Persistent=true
|
|
133
|
+
|
|
134
|
+
[Install]
|
|
135
|
+
WantedBy=timers.target
|
|
136
|
+
`;
|
|
137
|
+
fs.writeFileSync(path.join(UNIT_DIR, timerName), timerContent, "utf-8");
|
|
138
|
+
daemonReload();
|
|
139
|
+
try {
|
|
140
|
+
execSync(`systemctl --user enable --now ${timerName}`, { encoding: "utf-8" });
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
const e = err;
|
|
144
|
+
console.error(`Failed to enable timer ${timerName}: ${e.stderr || err}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
removeTaskTimer(taskId) {
|
|
149
|
+
const timerName = getTimerName(taskId);
|
|
150
|
+
const serviceName = getServiceName(taskId);
|
|
151
|
+
const timerPath = path.join(UNIT_DIR, timerName);
|
|
152
|
+
const servicePath = path.join(UNIT_DIR, serviceName);
|
|
153
|
+
if (fs.existsSync(timerPath)) {
|
|
154
|
+
try {
|
|
155
|
+
execSync(`systemctl --user stop ${timerName}`, { encoding: "utf-8" });
|
|
156
|
+
}
|
|
157
|
+
catch { /* timer might not be running */ }
|
|
158
|
+
try {
|
|
159
|
+
execSync(`systemctl --user disable ${timerName}`, { encoding: "utf-8" });
|
|
160
|
+
}
|
|
161
|
+
catch { /* timer might not be enabled */ }
|
|
162
|
+
fs.unlinkSync(timerPath);
|
|
163
|
+
}
|
|
164
|
+
if (fs.existsSync(servicePath))
|
|
165
|
+
fs.unlinkSync(servicePath);
|
|
166
|
+
daemonReload();
|
|
167
|
+
}
|
|
168
|
+
async startTask(taskId) {
|
|
169
|
+
const serviceName = getServiceName(taskId);
|
|
170
|
+
await execAsync(`systemctl --user start --no-block ${serviceName}`);
|
|
171
|
+
}
|
|
172
|
+
async stopTask(taskId) {
|
|
173
|
+
const serviceName = getServiceName(taskId);
|
|
174
|
+
await execAsync(`systemctl --user stop ${serviceName}`);
|
|
175
|
+
}
|
|
176
|
+
getGuiEnv() {
|
|
177
|
+
const uid = process.getuid?.();
|
|
178
|
+
const runtimeDir = process.env.XDG_RUNTIME_DIR ||
|
|
179
|
+
(uid !== undefined ? `/run/user/${uid}` : "");
|
|
180
|
+
return {
|
|
181
|
+
DISPLAY: ":0",
|
|
182
|
+
...(runtimeDir ? { XDG_RUNTIME_DIR: runtimeDir } : {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=linux.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HostConfig, ParsedTask } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Abstracts OS-specific daemon, scheduling, and process management.
|
|
4
|
+
* Linux uses systemd; Windows uses Task Scheduler; macOS will use launchd.
|
|
5
|
+
*/
|
|
6
|
+
export interface PlatformService {
|
|
7
|
+
/** Install the main `palmier serve` daemon to start at boot. */
|
|
8
|
+
installDaemon(config: HostConfig): void;
|
|
9
|
+
/** Install a scheduled trigger (timer) for a task. */
|
|
10
|
+
installTaskTimer(config: HostConfig, task: ParsedTask): void;
|
|
11
|
+
/** Remove a task's scheduled trigger and service files. */
|
|
12
|
+
removeTaskTimer(taskId: string): void;
|
|
13
|
+
/** Start a task execution (non-blocking). */
|
|
14
|
+
startTask(taskId: string): Promise<void>;
|
|
15
|
+
/** Abort/stop a running task. */
|
|
16
|
+
stopTask(taskId: string): Promise<void>;
|
|
17
|
+
/** Return env vars needed for GUI access (Linux: DISPLAY, etc.). */
|
|
18
|
+
getGuiEnv(): Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=platform.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PlatformService } from "./platform.js";
|
|
2
|
+
import type { HostConfig, ParsedTask } from "../types.js";
|
|
3
|
+
export declare class WindowsPlatform implements PlatformService {
|
|
4
|
+
installDaemon(config: HostConfig): void;
|
|
5
|
+
installTaskTimer(config: HostConfig, task: ParsedTask): void;
|
|
6
|
+
removeTaskTimer(taskId: string): void;
|
|
7
|
+
startTask(taskId: string): Promise<void>;
|
|
8
|
+
stopTask(taskId: string): Promise<void>;
|
|
9
|
+
getGuiEnv(): Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=windows.d.ts.map
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { execSync, exec, spawn as nodeSpawn } from "child_process";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
import { getTaskDir } from "../task.js";
|
|
6
|
+
import { loadConfig } from "../config.js";
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
const TASK_PREFIX = "PalmierTask-";
|
|
9
|
+
const DAEMON_TASK_NAME = "PalmierDaemon";
|
|
10
|
+
const SHELL = "cmd.exe";
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the full command to invoke palmier, accounting for the fact that
|
|
13
|
+
* on Windows, globally-installed npm packages are .cmd shims.
|
|
14
|
+
*/
|
|
15
|
+
function getPalmierCommand() {
|
|
16
|
+
// process.argv[1] is the script path; wrap with node so it works as
|
|
17
|
+
// a Task Scheduler command without relying on file associations.
|
|
18
|
+
const script = process.argv[1] || "palmier";
|
|
19
|
+
return `"${process.execPath}" "${script}"`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Convert one of the 4 supported cron patterns to schtasks flags.
|
|
23
|
+
*
|
|
24
|
+
* Only these patterns (produced by the PWA UI) are handled:
|
|
25
|
+
* hourly: "0 * * * *" → /sc HOURLY
|
|
26
|
+
* daily: "MM HH * * *" → /sc DAILY /st HH:MM
|
|
27
|
+
* weekly: "MM HH * * D" → /sc WEEKLY /d <day> /st HH:MM
|
|
28
|
+
* monthly: "MM HH D * *" → /sc MONTHLY /d D /st HH:MM
|
|
29
|
+
*
|
|
30
|
+
* Arbitrary cron expressions (ranges, lists, step values) are NOT handled
|
|
31
|
+
* because the UI never generates them.
|
|
32
|
+
*/
|
|
33
|
+
function cronToSchtasksArgs(cron) {
|
|
34
|
+
const parts = cron.trim().split(/\s+/);
|
|
35
|
+
if (parts.length !== 5) {
|
|
36
|
+
throw new Error(`Invalid cron expression (expected 5 fields): ${cron}`);
|
|
37
|
+
}
|
|
38
|
+
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
|
|
39
|
+
// Map cron day-of-week numbers to schtasks day abbreviations
|
|
40
|
+
const dowMap = {
|
|
41
|
+
"0": "SUN", "1": "MON", "2": "TUE", "3": "WED",
|
|
42
|
+
"4": "THU", "5": "FRI", "6": "SAT", "7": "SUN",
|
|
43
|
+
};
|
|
44
|
+
const st = `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}`;
|
|
45
|
+
// Hourly: "0 * * * *"
|
|
46
|
+
if (hour === "*" && dayOfMonth === "*" && dayOfWeek === "*") {
|
|
47
|
+
return ["/sc", "HOURLY"];
|
|
48
|
+
}
|
|
49
|
+
// Weekly: "MM HH * * D"
|
|
50
|
+
if (dayOfMonth === "*" && dayOfWeek !== "*") {
|
|
51
|
+
const day = dowMap[dayOfWeek];
|
|
52
|
+
if (!day)
|
|
53
|
+
throw new Error(`Unsupported day-of-week: ${dayOfWeek}`);
|
|
54
|
+
return ["/sc", "WEEKLY", "/d", day, "/st", st];
|
|
55
|
+
}
|
|
56
|
+
// Monthly: "MM HH D * *"
|
|
57
|
+
if (dayOfMonth !== "*" && dayOfWeek === "*") {
|
|
58
|
+
return ["/sc", "MONTHLY", "/d", dayOfMonth, "/st", st];
|
|
59
|
+
}
|
|
60
|
+
// Daily: "MM HH * * *" (most common fallback)
|
|
61
|
+
return ["/sc", "DAILY", "/st", st];
|
|
62
|
+
}
|
|
63
|
+
function schtasksTaskName(taskId) {
|
|
64
|
+
return `${TASK_PREFIX}${taskId}`;
|
|
65
|
+
}
|
|
66
|
+
export class WindowsPlatform {
|
|
67
|
+
installDaemon(config) {
|
|
68
|
+
const cmd = getPalmierCommand();
|
|
69
|
+
// Try ONSTART first (requires elevation), fall back to ONLOGON
|
|
70
|
+
const baseArgs = [
|
|
71
|
+
"/create", "/tn", DAEMON_TASK_NAME,
|
|
72
|
+
"/tr", `${cmd} serve`,
|
|
73
|
+
"/rl", "HIGHEST",
|
|
74
|
+
"/f", // force overwrite if exists
|
|
75
|
+
];
|
|
76
|
+
try {
|
|
77
|
+
execSync(`schtasks ${[...baseArgs, "/sc", "ONSTART"].join(" ")}`, { encoding: "utf-8", shell: SHELL });
|
|
78
|
+
console.log(`Task Scheduler: "${DAEMON_TASK_NAME}" installed (runs at startup).`);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// ONSTART requires admin — fall back to ONLOGON which does not
|
|
82
|
+
try {
|
|
83
|
+
execSync(`schtasks ${[...baseArgs, "/sc", "ONLOGON"].join(" ")}`, { encoding: "utf-8", shell: SHELL });
|
|
84
|
+
console.log(`Task Scheduler: "${DAEMON_TASK_NAME}" installed (runs at logon).`);
|
|
85
|
+
console.log(" Tip: run as Administrator to use ONSTART instead.");
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.error(`Warning: failed to create scheduled task: ${err}`);
|
|
89
|
+
console.error("You may need to start palmier serve manually.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Start the daemon now
|
|
93
|
+
try {
|
|
94
|
+
execSync(`schtasks /run /tn ${DAEMON_TASK_NAME}`, { encoding: "utf-8", shell: SHELL });
|
|
95
|
+
console.log("Palmier daemon started.");
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
console.log("Note: could not start daemon immediately. It will start at next login/boot.");
|
|
99
|
+
}
|
|
100
|
+
console.log("\nHost initialization complete!");
|
|
101
|
+
}
|
|
102
|
+
installTaskTimer(config, task) {
|
|
103
|
+
const taskId = task.frontmatter.id;
|
|
104
|
+
const tn = schtasksTaskName(taskId);
|
|
105
|
+
const cmd = getPalmierCommand();
|
|
106
|
+
const triggers = task.frontmatter.triggers || [];
|
|
107
|
+
for (const trigger of triggers) {
|
|
108
|
+
if (trigger.type === "cron") {
|
|
109
|
+
const schedArgs = cronToSchtasksArgs(trigger.value);
|
|
110
|
+
const args = [
|
|
111
|
+
"/create", "/tn", tn,
|
|
112
|
+
"/tr", `${cmd} run ${taskId}`,
|
|
113
|
+
...schedArgs,
|
|
114
|
+
"/f",
|
|
115
|
+
];
|
|
116
|
+
try {
|
|
117
|
+
execSync(`schtasks ${args.join(" ")}`, { encoding: "utf-8", shell: SHELL });
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const e = err;
|
|
121
|
+
console.error(`Failed to create scheduled task ${tn}: ${e.stderr || err}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (trigger.type === "once") {
|
|
125
|
+
// "once" triggers use ISO datetime: "2026-03-28T09:00"
|
|
126
|
+
const [datePart, timePart] = trigger.value.split("T");
|
|
127
|
+
if (!datePart || !timePart) {
|
|
128
|
+
console.error(`Invalid once trigger value: ${trigger.value}`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// schtasks expects MM/DD/YYYY date format
|
|
132
|
+
const [year, month, day] = datePart.split("-");
|
|
133
|
+
const sd = `${month}/${day}/${year}`;
|
|
134
|
+
const st = timePart.slice(0, 5);
|
|
135
|
+
const args = [
|
|
136
|
+
"/create", "/tn", tn,
|
|
137
|
+
"/tr", `${cmd} run ${taskId}`,
|
|
138
|
+
"/sc", "ONCE", "/sd", sd, "/st", st,
|
|
139
|
+
"/f",
|
|
140
|
+
];
|
|
141
|
+
try {
|
|
142
|
+
execSync(`schtasks ${args.join(" ")}`, { encoding: "utf-8", shell: SHELL });
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const e = err;
|
|
146
|
+
console.error(`Failed to create once task ${tn}: ${e.stderr || err}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
removeTaskTimer(taskId) {
|
|
152
|
+
const tn = schtasksTaskName(taskId);
|
|
153
|
+
try {
|
|
154
|
+
execSync(`schtasks /delete /tn ${tn} /f`, { encoding: "utf-8", shell: SHELL });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Task might not exist — that's fine
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async startTask(taskId) {
|
|
161
|
+
const config = loadConfig();
|
|
162
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
163
|
+
const script = process.argv[1] || "palmier";
|
|
164
|
+
// Spawn a detached child process and record its PID for later abort
|
|
165
|
+
const child = nodeSpawn(process.execPath, [script, "run", taskId], {
|
|
166
|
+
detached: true,
|
|
167
|
+
stdio: "ignore",
|
|
168
|
+
cwd: config.projectRoot,
|
|
169
|
+
});
|
|
170
|
+
if (child.pid) {
|
|
171
|
+
fs.mkdirSync(taskDir, { recursive: true });
|
|
172
|
+
fs.writeFileSync(path.join(taskDir, "pid"), String(child.pid), "utf-8");
|
|
173
|
+
}
|
|
174
|
+
child.unref();
|
|
175
|
+
}
|
|
176
|
+
async stopTask(taskId) {
|
|
177
|
+
const config = loadConfig();
|
|
178
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
179
|
+
const pidPath = path.join(taskDir, "pid");
|
|
180
|
+
if (!fs.existsSync(pidPath)) {
|
|
181
|
+
throw new Error(`No PID file found for task ${taskId}`);
|
|
182
|
+
}
|
|
183
|
+
const pid = fs.readFileSync(pidPath, "utf-8").trim();
|
|
184
|
+
try {
|
|
185
|
+
// /t kills the entire process tree, /f forces termination
|
|
186
|
+
await execAsync(`taskkill /pid ${pid} /t /f`);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
// Clean up PID file regardless of whether taskkill succeeded
|
|
190
|
+
try {
|
|
191
|
+
fs.unlinkSync(pidPath);
|
|
192
|
+
}
|
|
193
|
+
catch { /* ignore */ }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
getGuiEnv() {
|
|
197
|
+
// Windows GUI is always available — no special env vars needed
|
|
198
|
+
return {};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=windows.js.map
|
package/dist/rpc-handler.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import * as os from "os";
|
|
3
|
-
import { execSync, exec } from "child_process";
|
|
4
|
-
import { promisify } from "util";
|
|
5
|
-
const execAsync = promisify(exec);
|
|
6
3
|
import * as fs from "fs";
|
|
7
4
|
import * as path from "path";
|
|
8
5
|
import { fileURLToPath } from "url";
|
|
9
6
|
import { parse as parseYaml } from "yaml";
|
|
10
7
|
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, getTaskCreatedAt, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList } from "./task.js";
|
|
11
|
-
import {
|
|
8
|
+
import { getPlatform } from "./platform/index.js";
|
|
12
9
|
import { spawnCommand } from "./spawn-command.js";
|
|
13
10
|
import { getAgent } from "./agents/agent.js";
|
|
14
|
-
import {
|
|
11
|
+
import { validateSession } from "./session-store.js";
|
|
15
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
13
|
const PLAN_GENERATION_PROMPT = fs.readFileSync(path.join(__dirname, "commands", "plan-generation.md"), "utf-8");
|
|
17
14
|
function detectLanIp() {
|
|
@@ -110,11 +107,9 @@ export function createRpcHandler(config) {
|
|
|
110
107
|
};
|
|
111
108
|
}
|
|
112
109
|
async function handleRpc(request) {
|
|
113
|
-
// Session token validation:
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
return { error: "Unauthorized" };
|
|
117
|
-
}
|
|
110
|
+
// Session token validation: always require a valid session token
|
|
111
|
+
if (!request.sessionToken || !validateSession(request.sessionToken)) {
|
|
112
|
+
return { error: "Unauthorized" };
|
|
118
113
|
}
|
|
119
114
|
switch (request.method) {
|
|
120
115
|
case "task.list": {
|
|
@@ -159,8 +154,9 @@ export function createRpcHandler(config) {
|
|
|
159
154
|
};
|
|
160
155
|
writeTaskFile(taskDir, task);
|
|
161
156
|
appendTaskList(config.projectRoot, id);
|
|
157
|
+
const platform = getPlatform();
|
|
162
158
|
if (task.frontmatter.triggers_enabled) {
|
|
163
|
-
installTaskTimer(config, task);
|
|
159
|
+
platform.installTaskTimer(config, task);
|
|
164
160
|
}
|
|
165
161
|
return flattenTask(task);
|
|
166
162
|
}
|
|
@@ -203,23 +199,23 @@ export function createRpcHandler(config) {
|
|
|
203
199
|
}
|
|
204
200
|
writeTaskFile(taskDir, existing);
|
|
205
201
|
// Reinstall or remove timers based on triggers_enabled
|
|
206
|
-
|
|
202
|
+
const platform = getPlatform();
|
|
203
|
+
platform.removeTaskTimer(params.id);
|
|
207
204
|
if (existing.frontmatter.triggers_enabled) {
|
|
208
|
-
installTaskTimer(config, existing);
|
|
205
|
+
platform.installTaskTimer(config, existing);
|
|
209
206
|
}
|
|
210
207
|
return flattenTask(existing);
|
|
211
208
|
}
|
|
212
209
|
case "task.delete": {
|
|
213
210
|
const params = request.params;
|
|
214
|
-
removeTaskTimer(params.id);
|
|
211
|
+
getPlatform().removeTaskTimer(params.id);
|
|
215
212
|
removeFromTaskList(config.projectRoot, params.id);
|
|
216
213
|
return { ok: true, task_id: params.id };
|
|
217
214
|
}
|
|
218
215
|
case "task.run": {
|
|
219
216
|
const params = request.params;
|
|
220
|
-
const serviceName = `palmier-task-${params.id}.service`;
|
|
221
217
|
try {
|
|
222
|
-
await
|
|
218
|
+
await getPlatform().startTask(params.id);
|
|
223
219
|
return { ok: true, task_id: params.id };
|
|
224
220
|
}
|
|
225
221
|
catch (err) {
|
|
@@ -230,9 +226,8 @@ export function createRpcHandler(config) {
|
|
|
230
226
|
}
|
|
231
227
|
case "task.abort": {
|
|
232
228
|
const params = request.params;
|
|
233
|
-
const serviceName = `palmier-task-${params.id}.service`;
|
|
234
229
|
try {
|
|
235
|
-
await
|
|
230
|
+
await getPlatform().stopTask(params.id);
|
|
236
231
|
return { ok: true, task_id: params.id };
|
|
237
232
|
}
|
|
238
233
|
catch (err) {
|
|
@@ -241,18 +236,6 @@ export function createRpcHandler(config) {
|
|
|
241
236
|
return { error: `Failed to abort task: ${e.stderr || e.message}` };
|
|
242
237
|
}
|
|
243
238
|
}
|
|
244
|
-
case "task.logs": {
|
|
245
|
-
const params = request.params;
|
|
246
|
-
const serviceName = `palmier-task-${params.id}.service`;
|
|
247
|
-
try {
|
|
248
|
-
const logs = execSync(`journalctl --user -u ${serviceName} -n 100 --no-pager`, { encoding: "utf-8" });
|
|
249
|
-
return { task_id: params.id, logs };
|
|
250
|
-
}
|
|
251
|
-
catch (err) {
|
|
252
|
-
const error = err;
|
|
253
|
-
return { task_id: params.id, logs: error.stdout || "", error: error.stderr };
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
239
|
case "task.status": {
|
|
257
240
|
const params = request.params;
|
|
258
241
|
const taskDir = getTaskDir(config.projectRoot, params.id);
|
package/dist/spawn-command.d.ts
CHANGED
|
@@ -12,8 +12,11 @@ export interface SpawnCommandOptions {
|
|
|
12
12
|
/**
|
|
13
13
|
* Spawn a command with additional arguments.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* On Windows, `shell: true` is used so that npm-installed .cmd shims
|
|
16
|
+
* (e.g. claude.cmd, gemini.cmd) are resolved correctly.
|
|
17
|
+
*
|
|
18
|
+
* On other platforms the command is executed directly (no shell), so no
|
|
19
|
+
* escaping is needed.
|
|
17
20
|
*
|
|
18
21
|
* stdin is set to "ignore" (equivalent to < /dev/null) because tools like
|
|
19
22
|
* `claude -p` hang indefinitely on an open stdin pipe.
|
package/dist/spawn-command.js
CHANGED
|
@@ -5,6 +5,8 @@ export function spawnCommand(command, args, opts) {
|
|
|
5
5
|
cwd: opts.cwd,
|
|
6
6
|
stdio: ["ignore", "pipe", "pipe"],
|
|
7
7
|
env: opts.env ? { ...process.env, ...opts.env } : undefined,
|
|
8
|
+
// On Windows, spawn through shell so .cmd shims resolve correctly
|
|
9
|
+
shell: process.platform === "win32",
|
|
8
10
|
});
|
|
9
11
|
const chunks = [];
|
|
10
12
|
child.stdout.on("data", (d) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
2
|
import * as os from "os";
|
|
3
|
-
import { validateSession,
|
|
3
|
+
import { validateSession, addSession } from "../session-store.js";
|
|
4
4
|
const pendingPairs = new Map();
|
|
5
5
|
export function detectLanIp() {
|
|
6
6
|
const interfaces = os.networkInterfaces();
|
|
@@ -35,12 +35,7 @@ export async function startHttpTransport(config, handleRpc) {
|
|
|
35
35
|
if (!auth || !auth.startsWith("Bearer "))
|
|
36
36
|
return false;
|
|
37
37
|
const token = auth.slice(7);
|
|
38
|
-
|
|
39
|
-
if (token === config.directToken)
|
|
40
|
-
return true;
|
|
41
|
-
if (hasSessions() && validateSession(token))
|
|
42
|
-
return true;
|
|
43
|
-
return false;
|
|
38
|
+
return validateSession(token);
|
|
44
39
|
}
|
|
45
40
|
function extractSessionToken(req) {
|
|
46
41
|
const auth = req.headers.authorization;
|
package/package.json
CHANGED
package/src/agents/claude.ts
CHANGED
|
@@ -3,6 +3,10 @@ import { execSync } from "child_process";
|
|
|
3
3
|
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
4
|
import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
|
|
5
5
|
|
|
6
|
+
// execSync's shell option takes a string (shell path), not boolean.
|
|
7
|
+
// On Windows we need a shell so .cmd shims resolve correctly.
|
|
8
|
+
const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
9
|
+
|
|
6
10
|
export class ClaudeAgent implements AgentTool {
|
|
7
11
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
8
12
|
return {
|
|
@@ -25,12 +29,12 @@ export class ClaudeAgent implements AgentTool {
|
|
|
25
29
|
|
|
26
30
|
async init(): Promise<boolean> {
|
|
27
31
|
try {
|
|
28
|
-
execSync("claude --version");
|
|
32
|
+
execSync("claude --version", { shell: SHELL });
|
|
29
33
|
} catch {
|
|
30
34
|
return false;
|
|
31
35
|
}
|
|
32
36
|
try {
|
|
33
|
-
execSync("claude mcp add --transport stdio palmier --scope user -- palmier mcpserver");
|
|
37
|
+
execSync("claude mcp add --transport stdio palmier --scope user -- palmier mcpserver", { shell: SHELL });
|
|
34
38
|
} catch (err) {
|
|
35
39
|
console.warn("Warning: failed to install MCP for Claude:", err instanceof Error ? err.message : err);
|
|
36
40
|
}
|
package/src/agents/codex.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { execSync } from "child_process";
|
|
|
3
3
|
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
4
|
import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
|
|
5
5
|
|
|
6
|
+
// On Windows we need a shell so .cmd shims resolve correctly.
|
|
7
|
+
const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
8
|
+
|
|
6
9
|
export class CodexAgent implements AgentTool {
|
|
7
10
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
8
11
|
// TODO: fill in
|
|
@@ -32,12 +35,12 @@ export class CodexAgent implements AgentTool {
|
|
|
32
35
|
|
|
33
36
|
async init(): Promise<boolean> {
|
|
34
37
|
try {
|
|
35
|
-
execSync("codex --version");
|
|
38
|
+
execSync("codex --version", { shell: SHELL });
|
|
36
39
|
} catch {
|
|
37
40
|
return false;
|
|
38
41
|
}
|
|
39
42
|
try {
|
|
40
|
-
execSync("codex mcp add palmier palmier mcpserver");
|
|
43
|
+
execSync("codex mcp add palmier palmier mcpserver", { shell: SHELL });
|
|
41
44
|
} catch (err) {
|
|
42
45
|
console.warn("Warning: failed to install MCP for Codex:", err instanceof Error ? err.message : err);
|
|
43
46
|
}
|
package/src/agents/gemini.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { execSync } from "child_process";
|
|
|
3
3
|
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
4
|
import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
|
|
5
5
|
|
|
6
|
+
// On Windows we need a shell so .cmd shims resolve correctly.
|
|
7
|
+
const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
8
|
+
|
|
6
9
|
export class GeminiAgent implements AgentTool {
|
|
7
10
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
8
11
|
// TODO: fill in
|
|
@@ -29,12 +32,12 @@ export class GeminiAgent implements AgentTool {
|
|
|
29
32
|
|
|
30
33
|
async init(): Promise<boolean> {
|
|
31
34
|
try {
|
|
32
|
-
execSync("gemini --version");
|
|
35
|
+
execSync("gemini --version", { shell: SHELL });
|
|
33
36
|
} catch {
|
|
34
37
|
return false;
|
|
35
38
|
}
|
|
36
39
|
try {
|
|
37
|
-
execSync("gemini mcp add --scope user palmier palmier mcpserver");
|
|
40
|
+
execSync("gemini mcp add --scope user palmier palmier mcpserver", { shell: SHELL });
|
|
38
41
|
} catch (err) {
|
|
39
42
|
console.warn("Warning: failed to install MCP for Gemini:", err instanceof Error ? err.message : err);
|
|
40
43
|
}
|