palmier 0.4.2 → 0.4.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/README.md +18 -30
- package/dist/agents/agent-instructions.md +40 -0
- package/dist/agents/claude.js +2 -8
- package/dist/agents/codex.js +0 -6
- package/dist/agents/copilot.js +0 -20
- package/dist/agents/gemini.js +0 -6
- package/dist/agents/shared-prompt.d.ts +1 -2
- package/dist/agents/shared-prompt.js +5 -18
- package/dist/commands/notify.d.ts +9 -0
- package/dist/commands/notify.js +43 -0
- package/dist/commands/request-input.d.ts +10 -0
- package/dist/commands/request-input.js +49 -0
- package/dist/commands/run.d.ts +4 -5
- package/dist/commands/run.js +90 -105
- package/dist/commands/serve.js +31 -28
- package/dist/index.js +15 -5
- package/dist/platform/linux.js +16 -6
- package/dist/platform/windows.js +54 -14
- package/dist/rpc-handler.js +217 -54
- package/dist/spawn-command.d.ts +1 -1
- package/dist/spawn-command.js +13 -1
- package/dist/task.d.ts +18 -7
- package/dist/task.js +70 -27
- package/dist/types.d.ts +10 -1
- package/package.json +2 -3
- package/src/agents/agent-instructions.md +40 -0
- package/src/agents/claude.ts +2 -7
- package/src/agents/codex.ts +0 -5
- package/src/agents/copilot.ts +0 -19
- package/src/agents/gemini.ts +0 -5
- package/src/agents/shared-prompt.ts +10 -18
- package/src/commands/notify.ts +44 -0
- package/src/commands/request-input.ts +51 -0
- package/src/commands/run.ts +98 -129
- package/src/commands/serve.ts +34 -36
- package/src/index.ts +16 -5
- package/src/platform/linux.ts +17 -7
- package/src/platform/windows.ts +53 -15
- package/src/rpc-handler.ts +244 -57
- package/src/spawn-command.ts +13 -2
- package/src/task.ts +79 -29
- package/src/types.ts +11 -1
- package/dist/commands/mcpserver.d.ts +0 -2
- package/dist/commands/mcpserver.js +0 -93
- package/src/commands/mcpserver.ts +0 -113
package/src/commands/serve.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { loadConfig } from "../config.js";
|
|
|
4
4
|
import { connectNats } from "../nats-client.js";
|
|
5
5
|
import { createRpcHandler } from "../rpc-handler.js";
|
|
6
6
|
import { startNatsTransport } from "../transports/nats-transport.js";
|
|
7
|
-
import { getTaskDir, readTaskStatus, writeTaskStatus,
|
|
7
|
+
import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage } from "../task.js";
|
|
8
8
|
import { publishHostEvent } from "../events.js";
|
|
9
9
|
import { getPlatform } from "../platform/index.js";
|
|
10
10
|
import { detectAgents } from "../agents/agent.js";
|
|
@@ -17,41 +17,11 @@ const POLL_INTERVAL_MS = 30_000;
|
|
|
17
17
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
* and broadcast the failure event.
|
|
22
|
-
*/
|
|
23
|
-
async function markTaskFailed(
|
|
24
|
-
config: HostConfig,
|
|
25
|
-
nc: NatsConnection | undefined,
|
|
26
|
-
taskId: string,
|
|
27
|
-
reason: string,
|
|
28
|
-
): Promise<void> {
|
|
29
|
-
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
30
|
-
const status = readTaskStatus(taskDir);
|
|
31
|
-
if (!status || status.running_state !== "started") return;
|
|
32
|
-
|
|
33
|
-
console.log(`[monitor] Task ${taskId} ${reason}, marking as failed.`);
|
|
34
|
-
const endTime = Date.now();
|
|
35
|
-
writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
|
|
36
|
-
|
|
37
|
-
let taskName = taskId;
|
|
38
|
-
try {
|
|
39
|
-
const task = parseTaskFile(taskDir);
|
|
40
|
-
taskName = task.frontmatter.name || taskId;
|
|
41
|
-
} catch { /* use taskId as fallback */ }
|
|
42
|
-
|
|
43
|
-
const resultFileName = `RESULT-${endTime}.md`;
|
|
44
|
-
const content = `---\ntask_name: ${taskName}\nrunning_state: failed\nstart_time: ${status.time_stamp}\nend_time: ${endTime}\ntask_file: \n---\n${reason}`;
|
|
45
|
-
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
46
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
47
|
-
|
|
48
|
-
const payload: Record<string, unknown> = { event_type: "running-state", running_state: "failed", name: taskName };
|
|
49
|
-
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Scan all tasks for any stuck in "start" state whose process is no longer alive.
|
|
20
|
+
* Scan all tasks for any stuck in "started" state whose process is no longer alive.
|
|
54
21
|
* Uses the system scheduler (Task Scheduler / systemd) as the authoritative source.
|
|
22
|
+
*
|
|
23
|
+
* Since run.ts creates the RESULT file and history entry at start, we just need to
|
|
24
|
+
* finalize the existing RESULT file, append a failed status entry, and broadcast.
|
|
55
25
|
*/
|
|
56
26
|
async function checkStaleTasks(
|
|
57
27
|
config: HostConfig,
|
|
@@ -75,7 +45,35 @@ async function checkStaleTasks(
|
|
|
75
45
|
// Ask the system scheduler if the task is still running
|
|
76
46
|
if (platform.isTaskRunning(taskId)) continue;
|
|
77
47
|
|
|
78
|
-
|
|
48
|
+
console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
|
|
49
|
+
const endTime = Date.now();
|
|
50
|
+
writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
|
|
51
|
+
|
|
52
|
+
// Find the latest run directory (created by run.ts at start)
|
|
53
|
+
const runId = fs.readdirSync(taskDir)
|
|
54
|
+
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
|
|
55
|
+
.sort()
|
|
56
|
+
.pop();
|
|
57
|
+
|
|
58
|
+
if (runId) {
|
|
59
|
+
appendRunMessage(taskDir, runId, {
|
|
60
|
+
role: "status",
|
|
61
|
+
time: endTime,
|
|
62
|
+
content: "",
|
|
63
|
+
type: "failed",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let taskName = taskId;
|
|
68
|
+
try {
|
|
69
|
+
taskName = parseTaskFile(taskDir).frontmatter.name || taskId;
|
|
70
|
+
} catch { /* use taskId as fallback */ }
|
|
71
|
+
|
|
72
|
+
await publishHostEvent(nc, config.hostId, taskId, {
|
|
73
|
+
event_type: "running-state",
|
|
74
|
+
running_state: "failed",
|
|
75
|
+
name: taskName,
|
|
76
|
+
});
|
|
79
77
|
}
|
|
80
78
|
}
|
|
81
79
|
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { initCommand } from "./commands/init.js";
|
|
|
9
9
|
import { infoCommand } from "./commands/info.js";
|
|
10
10
|
import { runCommand } from "./commands/run.js";
|
|
11
11
|
import { serveCommand } from "./commands/serve.js";
|
|
12
|
-
import {
|
|
12
|
+
import { notifyCommand } from "./commands/notify.js";
|
|
13
|
+
import { requestInputCommand } from "./commands/request-input.js";
|
|
13
14
|
|
|
14
15
|
import { pairCommand } from "./commands/pair.js";
|
|
15
16
|
import { lanCommand } from "./commands/lan.js";
|
|
@@ -62,10 +63,20 @@ program
|
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
program
|
|
65
|
-
.command("
|
|
66
|
-
.description("
|
|
67
|
-
.
|
|
68
|
-
|
|
66
|
+
.command("notify")
|
|
67
|
+
.description("Send a push notification to the user")
|
|
68
|
+
.requiredOption("--title <title>", "Notification title")
|
|
69
|
+
.requiredOption("--body <body>", "Notification body text")
|
|
70
|
+
.action(async (opts: { title: string; body: string }) => {
|
|
71
|
+
await notifyCommand(opts);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
program
|
|
75
|
+
.command("request-input")
|
|
76
|
+
.description("Request input from the user (requires PALMIER_TASK_ID env var)")
|
|
77
|
+
.requiredOption("--description <desc...>", "Input descriptions to show the user")
|
|
78
|
+
.action(async (opts: { description: string[] }) => {
|
|
79
|
+
await requestInputCommand(opts);
|
|
69
80
|
});
|
|
70
81
|
|
|
71
82
|
program
|
package/src/platform/linux.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { execSync, exec } from "child_process";
|
|
|
5
5
|
import { promisify } from "util";
|
|
6
6
|
import type { PlatformService } from "./platform.js";
|
|
7
7
|
import type { HostConfig, ParsedTask } from "../types.js";
|
|
8
|
+
import { loadConfig } from "../config.js";
|
|
9
|
+
import { getTaskDir, readTaskStatus } from "../task.js";
|
|
8
10
|
|
|
9
11
|
const execAsync = promisify(exec);
|
|
10
12
|
|
|
@@ -230,20 +232,28 @@ WantedBy=timers.target
|
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
isTaskRunning(taskId: string): boolean {
|
|
235
|
+
// Check systemd first (for scheduled/on-demand runs)
|
|
233
236
|
const serviceName = getServiceName(taskId);
|
|
234
237
|
try {
|
|
235
|
-
// is-active exits 0 only for "active". For oneshot services (Type=oneshot),
|
|
236
|
-
// the state is "activating" while running, which exits non-zero.
|
|
237
|
-
// Use show -p ActiveState to reliably get the state without exit code issues.
|
|
238
238
|
const out = execSync(
|
|
239
239
|
`systemctl --user show -p ActiveState --value ${serviceName}`,
|
|
240
240
|
{ encoding: "utf-8" },
|
|
241
241
|
);
|
|
242
242
|
const state = out.trim();
|
|
243
|
-
|
|
244
|
-
} catch {
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
if (state === "active" || state === "activating") return true;
|
|
244
|
+
} catch { /* service may not exist */ }
|
|
245
|
+
|
|
246
|
+
// Fall back to PID check (for follow-up runs spawned directly)
|
|
247
|
+
try {
|
|
248
|
+
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
249
|
+
const status = readTaskStatus(taskDir);
|
|
250
|
+
if (status?.pid) {
|
|
251
|
+
process.kill(status.pid, 0); // signal 0 = check if process exists
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
} catch { /* process not running or config unavailable */ }
|
|
255
|
+
|
|
256
|
+
return false;
|
|
247
257
|
}
|
|
248
258
|
|
|
249
259
|
getGuiEnv(): Record<string, string> {
|
package/src/platform/windows.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { execFileSync } from "child_process";
|
|
|
4
4
|
import { spawn as nodeSpawn } from "child_process";
|
|
5
5
|
import type { PlatformService } from "./platform.js";
|
|
6
6
|
import type { HostConfig, ParsedTask } from "../types.js";
|
|
7
|
-
import { CONFIG_DIR } from "../config.js";
|
|
7
|
+
import { CONFIG_DIR, loadConfig } from "../config.js";
|
|
8
|
+
import { getTaskDir, readTaskStatus } from "../task.js";
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
const TASK_PREFIX = "\\Palmier\\PalmierTask-";
|
|
@@ -12,14 +13,6 @@ const DAEMON_TASK_NAME = "PalmierDaemon";
|
|
|
12
13
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
13
14
|
const DAEMON_VBS_FILE = path.join(CONFIG_DIR, "daemon.vbs");
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
* Build the /tr value for schtasks: a single string with quoted paths
|
|
17
|
-
* so Task Scheduler can invoke node with the palmier script + subcommand.
|
|
18
|
-
*/
|
|
19
|
-
function schtasksTr(...subcommand: string[]): string {
|
|
20
|
-
const script = process.argv[1] || "palmier";
|
|
21
|
-
return `"${process.execPath}" "${script}" ${subcommand.join(" ")}`;
|
|
22
|
-
}
|
|
23
16
|
|
|
24
17
|
const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
25
18
|
|
|
@@ -140,7 +133,7 @@ export class WindowsPlatform implements PlatformService {
|
|
|
140
133
|
// Kill old daemon first, then spawn new one.
|
|
141
134
|
if (oldPid) {
|
|
142
135
|
try {
|
|
143
|
-
execFileSync("taskkill", ["/pid", oldPid, "/f"], { windowsHide: true, stdio: "pipe" });
|
|
136
|
+
execFileSync("taskkill", ["/pid", oldPid, "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
144
137
|
} catch {
|
|
145
138
|
// Process may have already exited
|
|
146
139
|
}
|
|
@@ -168,7 +161,15 @@ export class WindowsPlatform implements PlatformService {
|
|
|
168
161
|
installTaskTimer(config: HostConfig, task: ParsedTask): void {
|
|
169
162
|
const taskId = task.frontmatter.id;
|
|
170
163
|
const tn = schtasksTaskName(taskId);
|
|
171
|
-
const
|
|
164
|
+
const script = process.argv[1] || "palmier";
|
|
165
|
+
|
|
166
|
+
// Write a VBS launcher so the task runs without a visible console window
|
|
167
|
+
const vbsPath = path.join(CONFIG_DIR, `task-${taskId}.vbs`);
|
|
168
|
+
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" run ${taskId}", 0, True`;
|
|
169
|
+
fs.writeFileSync(vbsPath, vbs, "utf-8");
|
|
170
|
+
|
|
171
|
+
const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
|
|
172
|
+
const tr = `"${wscript}" "${vbsPath}"`;
|
|
172
173
|
|
|
173
174
|
// Build trigger XML elements
|
|
174
175
|
const triggerElements: string[] = [];
|
|
@@ -212,6 +213,7 @@ export class WindowsPlatform implements PlatformService {
|
|
|
212
213
|
} catch {
|
|
213
214
|
// Task might not exist — that's fine
|
|
214
215
|
}
|
|
216
|
+
try { fs.unlinkSync(path.join(CONFIG_DIR, `task-${taskId}.vbs`)); } catch { /* ignore */ }
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
async startTask(taskId: string): Promise<void> {
|
|
@@ -225,6 +227,20 @@ export class WindowsPlatform implements PlatformService {
|
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
async stopTask(taskId: string): Promise<void> {
|
|
230
|
+
// Try to kill the entire process tree via the PID recorded in status.json.
|
|
231
|
+
// schtasks /end only kills the top-level process, leaving agent children orphaned.
|
|
232
|
+
try {
|
|
233
|
+
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
234
|
+
const status = readTaskStatus(taskDir);
|
|
235
|
+
if (status?.pid) {
|
|
236
|
+
execFileSync("taskkill", ["/pid", String(status.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// PID may be stale or config unavailable; fall through to schtasks /end
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Fallback: schtasks /end (kills top-level process only)
|
|
228
244
|
const tn = schtasksTaskName(taskId);
|
|
229
245
|
try {
|
|
230
246
|
execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true });
|
|
@@ -235,16 +251,38 @@ export class WindowsPlatform implements PlatformService {
|
|
|
235
251
|
}
|
|
236
252
|
|
|
237
253
|
isTaskRunning(taskId: string): boolean {
|
|
254
|
+
// Check Task Scheduler first (for scheduled/on-demand runs)
|
|
238
255
|
const tn = schtasksTaskName(taskId);
|
|
239
256
|
try {
|
|
240
257
|
const out = execFileSync("schtasks", ["/query", "/tn", tn, "/fo", "CSV", "/nh"], {
|
|
241
258
|
encoding: "utf-8",
|
|
242
259
|
windowsHide: true,
|
|
243
260
|
});
|
|
244
|
-
|
|
245
|
-
} catch {
|
|
246
|
-
|
|
247
|
-
|
|
261
|
+
if (out.includes('"Running"')) return true;
|
|
262
|
+
} catch { /* task may not exist in scheduler */ }
|
|
263
|
+
|
|
264
|
+
// Fall back to PID check (for follow-up runs spawned directly, not via schtasks)
|
|
265
|
+
try {
|
|
266
|
+
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
267
|
+
const status = readTaskStatus(taskDir);
|
|
268
|
+
if (status?.pid) {
|
|
269
|
+
// tasklist exits 0 if the PID is found
|
|
270
|
+
execFileSync("tasklist", ["/fi", `PID eq ${status.pid}`, "/nh"], {
|
|
271
|
+
encoding: "utf-8",
|
|
272
|
+
windowsHide: true,
|
|
273
|
+
stdio: "pipe",
|
|
274
|
+
});
|
|
275
|
+
// tasklist always exits 0; check if output contains the PID
|
|
276
|
+
const out = execFileSync("tasklist", ["/fi", `PID eq ${status.pid}`, "/fo", "CSV", "/nh"], {
|
|
277
|
+
encoding: "utf-8",
|
|
278
|
+
windowsHide: true,
|
|
279
|
+
stdio: "pipe",
|
|
280
|
+
});
|
|
281
|
+
if (out.includes(`"${status.pid}"`)) return true;
|
|
282
|
+
}
|
|
283
|
+
} catch { /* ignore */ }
|
|
284
|
+
|
|
285
|
+
return false;
|
|
248
286
|
}
|
|
249
287
|
|
|
250
288
|
getGuiEnv(): Record<string, string> {
|