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.
Files changed (45) hide show
  1. package/README.md +18 -30
  2. package/dist/agents/agent-instructions.md +40 -0
  3. package/dist/agents/claude.js +2 -8
  4. package/dist/agents/codex.js +0 -6
  5. package/dist/agents/copilot.js +0 -20
  6. package/dist/agents/gemini.js +0 -6
  7. package/dist/agents/shared-prompt.d.ts +1 -2
  8. package/dist/agents/shared-prompt.js +5 -18
  9. package/dist/commands/notify.d.ts +9 -0
  10. package/dist/commands/notify.js +43 -0
  11. package/dist/commands/request-input.d.ts +10 -0
  12. package/dist/commands/request-input.js +49 -0
  13. package/dist/commands/run.d.ts +4 -5
  14. package/dist/commands/run.js +90 -105
  15. package/dist/commands/serve.js +31 -28
  16. package/dist/index.js +15 -5
  17. package/dist/platform/linux.js +16 -6
  18. package/dist/platform/windows.js +54 -14
  19. package/dist/rpc-handler.js +217 -54
  20. package/dist/spawn-command.d.ts +1 -1
  21. package/dist/spawn-command.js +13 -1
  22. package/dist/task.d.ts +18 -7
  23. package/dist/task.js +70 -27
  24. package/dist/types.d.ts +10 -1
  25. package/package.json +2 -3
  26. package/src/agents/agent-instructions.md +40 -0
  27. package/src/agents/claude.ts +2 -7
  28. package/src/agents/codex.ts +0 -5
  29. package/src/agents/copilot.ts +0 -19
  30. package/src/agents/gemini.ts +0 -5
  31. package/src/agents/shared-prompt.ts +10 -18
  32. package/src/commands/notify.ts +44 -0
  33. package/src/commands/request-input.ts +51 -0
  34. package/src/commands/run.ts +98 -129
  35. package/src/commands/serve.ts +34 -36
  36. package/src/index.ts +16 -5
  37. package/src/platform/linux.ts +17 -7
  38. package/src/platform/windows.ts +53 -15
  39. package/src/rpc-handler.ts +244 -57
  40. package/src/spawn-command.ts +13 -2
  41. package/src/task.ts +79 -29
  42. package/src/types.ts +11 -1
  43. package/dist/commands/mcpserver.d.ts +0 -2
  44. package/dist/commands/mcpserver.js +0 -93
  45. package/src/commands/mcpserver.ts +0 -113
@@ -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, appendHistory, parseTaskFile } from "../task.js";
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
- * Mark a stuck task as failed: update status.json, write RESULT, append history,
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
- await markTaskFailed(config, nc, taskId, "Task process exited unexpectedly");
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 { mcpserverCommand } from "./commands/mcpserver.js";
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("mcpserver")
66
- .description("Start an MCP server exposing Palmier tools (stdio transport)")
67
- .action(async () => {
68
- await mcpserverCommand();
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
@@ -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
- return state === "active" || state === "activating";
244
- } catch {
245
- return false;
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> {
@@ -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 tr = schtasksTr("run", taskId);
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
- return out.includes('"Running"');
245
- } catch {
246
- return false;
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> {