palmier 0.5.3 → 0.5.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 CHANGED
@@ -53,6 +53,7 @@ All `palmier` commands should be run from a dedicated Palmier root directory (e.
53
53
  | `palmier serve` | Run the persistent RPC handler (default command) |
54
54
  | `palmier restart` | Restart the palmier serve daemon |
55
55
  | `palmier run <task-id>` | Execute a specific task |
56
+ | `palmier uninstall` | Stop daemon and remove all scheduled tasks |
56
57
 
57
58
  ## Setup
58
59
 
@@ -84,13 +85,15 @@ palmier clients revoke-all
84
85
  ```
85
86
 
86
87
  The `init` command:
87
- - Detects installed agent CLIs (Claude Code, Gemini CLI, Codex CLI, GitHub Copilot) and caches the result
88
+ - Detects installed agent CLIs (Claude Code, Gemini CLI, Codex CLI, GitHub Copilot, Qwen Code, Kimi Code, OpenClaw) and caches the result
88
89
  - Configures access modes (HTTP port, LAN access)
89
- - Shows a summary and asks for confirmation before making changes
90
+ - Shows a summary (including any existing scheduled tasks to recover) and asks for confirmation
90
91
  - Registers with the Palmier server, saves configuration to `~/.config/palmier/host.json`
91
- - Installs a background daemon (systemd user service on Linux, Registry Run key on Windows)
92
+ - Installs a background daemon (systemd user service on Linux, Task Scheduler on Windows)
92
93
  - Auto-enters pair mode to connect your first device
93
94
 
95
+ The daemon automatically recovers existing tasks by reinstalling their system timers on startup.
96
+
94
97
  Agents are re-detected on every daemon start. Run `palmier restart` after installing or removing a CLI.
95
98
 
96
99
  ### Verifying the Service
@@ -125,7 +128,7 @@ palmier restart
125
128
 
126
129
  ## How It Works
127
130
 
128
- - The host runs as a **background daemon** (systemd user service on Linux, Registry Run key on Windows), staying alive via `palmier serve`.
131
+ - The host runs as a **background daemon** (systemd user service on Linux, Task Scheduler on Windows), staying alive via `palmier serve`.
129
132
  - **Device access** — localhost is always trusted (no pairing needed). LAN and server mode devices communicate via direct HTTP or NATS respectively, and must pair via OTP to get a client token.
130
133
  - **Tasks** are stored locally as Markdown files in a `tasks/` directory. Each task has a name, prompt, execution plan, and optional schedules (cron schedules or one-time dates).
131
134
  - **Plan generation** is automatic — when you create or update a task, the host invokes your chosen agent CLI to generate an execution plan and name.
@@ -139,43 +142,30 @@ To fully remove Palmier from a machine:
139
142
 
140
143
  1. **Unpair your device** in the PWA (via the host menu).
141
144
 
142
- 2. **Stop and remove the daemon:**
145
+ 2. **Stop the daemon and remove all scheduled tasks:**
143
146
 
144
- **Linux:**
145
147
  ```bash
146
- systemctl --user stop palmier.service
147
- systemctl --user disable palmier.service
148
- rm ~/.config/systemd/user/palmier.service
148
+ palmier uninstall
149
149
  ```
150
150
 
151
- **Windows (PowerShell):**
152
- ```powershell
153
- # Kill the daemon process
154
- Get-Content "$env:USERPROFILE\.config\palmier\daemon.pid" | ForEach-Object { Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue }
155
- # Remove the Registry Run key
156
- Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name "PalmierDaemon" -ErrorAction SilentlyContinue
151
+ 3. **Uninstall the package:**
152
+
153
+ ```bash
154
+ npm uninstall -g palmier
157
155
  ```
158
156
 
159
- 3. **Remove any task timers:**
157
+ 4. **(Optional) Remove configuration and task data:**
160
158
 
161
159
  **Linux:**
162
160
  ```bash
163
- systemctl --user stop palmier-task-*.timer palmier-task-*.service 2>/dev/null
164
- systemctl --user disable palmier-task-*.timer 2>/dev/null
165
- rm -f ~/.config/systemd/user/palmier-task-*.timer ~/.config/systemd/user/palmier-task-*.service
166
- systemctl --user daemon-reload
161
+ rm -rf ~/.config/palmier
162
+ rm -rf ~/palmier # or wherever your Palmier root directory is
167
163
  ```
168
164
 
169
165
  **Windows (PowerShell):**
170
166
  ```powershell
171
- schtasks /delete /tn "PalmierTask-*" /f 2>$null
172
- ```
173
-
174
- 4. **Remove configuration and task data:**
175
-
176
- ```bash
177
- rm -rf ~/.config/palmier
178
- rm -rf tasks/ # from your Palmier root directory
167
+ Remove-Item -Recurse -Force "$env:USERPROFILE\.config\palmier"
168
+ Remove-Item -Recurse -Force "$env:USERPROFILE\palmier" # or wherever your Palmier root directory is
179
169
  ```
180
170
 
181
171
  ## Disclaimer
@@ -4,6 +4,7 @@ import { detectAgents } from "../agents/agent.js";
4
4
  import { getPlatform } from "../platform/index.js";
5
5
  import { pairCommand } from "./pair.js";
6
6
  import { detectLanIp } from "../transports/http-transport.js";
7
+ import { listTasks } from "../task.js";
7
8
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
8
9
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
9
10
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
@@ -51,6 +52,15 @@ export async function initCommand() {
51
52
  console.log(` Accessible from other devices on your local network. Pairing required.\n`);
52
53
  }
53
54
  console.log(` ${dim("Agents:")} ${agents.map((a) => a.label).join(", ")}\n`);
55
+ // Check for existing tasks to recover
56
+ const existingTasks = listTasks(process.cwd());
57
+ if (existingTasks.length > 0) {
58
+ console.log(` ${dim("Recover tasks:")} ${existingTasks.length} existing task(s) found:`);
59
+ for (const t of existingTasks) {
60
+ console.log(` - ${t.frontmatter.name || t.frontmatter.user_prompt.slice(0, 50)}`);
61
+ }
62
+ console.log();
63
+ }
54
64
  const confirm = await ask("Proceed? (Y/n): ");
55
65
  if (confirm.trim().toLowerCase() === "n") {
56
66
  console.log("\nSetup cancelled.");
@@ -96,6 +106,8 @@ export async function initCommand() {
96
106
  saveConfig(config);
97
107
  console.log(`Config saved to ${dim("~/.config/palmier/host.json")}`);
98
108
  getPlatform().installDaemon(config);
109
+ // Task recovery happens in the daemon (palmier serve) on startup,
110
+ // since the daemon runs elevated and can create S4U scheduled tasks.
99
111
  console.log("\nStarting pairing...");
100
112
  rl.close();
101
113
  await pairCommand();
@@ -5,7 +5,7 @@ import { connectNats } from "../nats-client.js";
5
5
  import { createRpcHandler } from "../rpc-handler.js";
6
6
  import { startNatsTransport } from "../transports/nats-transport.js";
7
7
  import { startHttpTransport } from "../transports/http-transport.js";
8
- import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage } from "../task.js";
8
+ import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage, listTasks } from "../task.js";
9
9
  import { publishHostEvent } from "../events.js";
10
10
  import { getPlatform } from "../platform/index.js";
11
11
  import { detectAgents } from "../agents/agent.js";
@@ -92,6 +92,17 @@ export async function serveCommand() {
92
92
  }
93
93
  // Reconcile any tasks stuck from before daemon started
94
94
  await checkStaleTasks(config, nc);
95
+ // Ensure all tasks have their scheduler entries (recovery after init/reinstall)
96
+ const platform = getPlatform();
97
+ const allTasks = listTasks(config.projectRoot);
98
+ for (const task of allTasks) {
99
+ try {
100
+ platform.installTaskTimer(config, task);
101
+ }
102
+ catch (err) {
103
+ console.error(`Warning: failed to install timer for task ${task.frontmatter.id}: ${err}`);
104
+ }
105
+ }
95
106
  // Poll for crashed tasks every 30 seconds
96
107
  setInterval(() => {
97
108
  checkStaleTasks(config, nc).catch((err) => {
@@ -0,0 +1,2 @@
1
+ export declare function uninstallCommand(): Promise<void>;
2
+ //# sourceMappingURL=uninstall.d.ts.map
@@ -0,0 +1,8 @@
1
+ import { getPlatform } from "../platform/index.js";
2
+ export async function uninstallCommand() {
3
+ const platform = getPlatform();
4
+ platform.uninstallDaemon();
5
+ console.log("\nTo uninstall the package: npm uninstall -g palmier");
6
+ console.log("To also remove configuration and task data, see https://github.com/caihongxu/palmier#uninstalling");
7
+ }
8
+ //# sourceMappingURL=uninstall.js.map
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import { serveCommand } from "./commands/serve.js";
11
11
  import { pairCommand } from "./commands/pair.js";
12
12
  import { restartCommand } from "./commands/restart.js";
13
13
  import { clientsListCommand, clientsRevokeCommand, clientsRevokeAllCommand } from "./commands/clients.js";
14
+ import { uninstallCommand } from "./commands/uninstall.js";
14
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
16
  const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
16
17
  const program = new Command();
@@ -75,6 +76,12 @@ clientsCmd
75
76
  .action(async () => {
76
77
  await clientsRevokeAllCommand();
77
78
  });
79
+ program
80
+ .command("uninstall")
81
+ .description("Stop the daemon and remove all scheduled tasks")
82
+ .action(async () => {
83
+ await uninstallCommand();
84
+ });
78
85
  // No subcommand → default to serve
79
86
  if (process.argv.length <= 2) {
80
87
  process.argv.push("serve");
@@ -14,6 +14,7 @@ import type { HostConfig, ParsedTask } from "../types.js";
14
14
  export declare function cronToOnCalendar(cron: string): string;
15
15
  export declare class LinuxPlatform implements PlatformService {
16
16
  installDaemon(config: HostConfig): void;
17
+ uninstallDaemon(): void;
17
18
  restartDaemon(): Promise<void>;
18
19
  installTaskTimer(config: HostConfig, task: ParsedTask): void;
19
20
  removeTaskTimer(taskId: string): void;
@@ -103,6 +103,44 @@ WantedBy=default.target
103
103
  }
104
104
  console.log("\nHost initialization complete!");
105
105
  }
106
+ uninstallDaemon() {
107
+ try {
108
+ execSync("systemctl --user stop palmier.service 2>/dev/null", { stdio: "pipe" });
109
+ execSync("systemctl --user disable palmier.service 2>/dev/null", { stdio: "pipe" });
110
+ }
111
+ catch { /* service may not exist */ }
112
+ // Remove daemon service file
113
+ const servicePath = path.join(UNIT_DIR, "palmier.service");
114
+ try {
115
+ fs.unlinkSync(servicePath);
116
+ }
117
+ catch { /* ignore */ }
118
+ // Remove all task timers and services
119
+ try {
120
+ const files = fs.readdirSync(UNIT_DIR).filter((f) => f.startsWith("palmier-task-"));
121
+ for (const f of files) {
122
+ const unit = f.replace(/\.(timer|service)$/, "");
123
+ try {
124
+ execSync(`systemctl --user stop ${f} 2>/dev/null`, { stdio: "pipe" });
125
+ }
126
+ catch { /* ignore */ }
127
+ try {
128
+ execSync(`systemctl --user disable ${f} 2>/dev/null`, { stdio: "pipe" });
129
+ }
130
+ catch { /* ignore */ }
131
+ try {
132
+ fs.unlinkSync(path.join(UNIT_DIR, f));
133
+ }
134
+ catch { /* ignore */ }
135
+ }
136
+ }
137
+ catch { /* ignore */ }
138
+ try {
139
+ execSync("systemctl --user daemon-reload", { stdio: "pipe" });
140
+ }
141
+ catch { /* ignore */ }
142
+ console.log("Palmier daemon and tasks uninstalled.");
143
+ }
106
144
  async restartDaemon() {
107
145
  // If called from a user's terminal, save the current PATH for future use.
108
146
  // If called from the daemon (auto-update), read the saved PATH instead.
@@ -8,6 +8,8 @@ export interface PlatformService {
8
8
  installDaemon(config: HostConfig): void;
9
9
  /** Restart the `palmier serve` daemon. */
10
10
  restartDaemon(): Promise<void>;
11
+ /** Stop the daemon and remove all scheduled tasks/timers. */
12
+ uninstallDaemon(): void;
11
13
  /** Install a scheduled trigger (timer) for a task. */
12
14
  installTaskTimer(config: HostConfig, task: ParsedTask): void;
13
15
  /** Remove a task's scheduled trigger and service files. */
@@ -16,11 +16,12 @@ export declare function triggerToXml(trigger: {
16
16
  /**
17
17
  * Build a complete Task Scheduler XML definition.
18
18
  */
19
- export declare function buildTaskXml(tr: string, triggers: string[]): string;
19
+ export declare function buildTaskXml(tr: string, triggers: string[], foreground?: boolean): string;
20
20
  export declare class WindowsPlatform implements PlatformService {
21
21
  installDaemon(config: HostConfig): void;
22
+ uninstallDaemon(): void;
22
23
  restartDaemon(): Promise<void>;
23
- /** Create or update the Task Scheduler entry for the daemon. */
24
+ /** Create or update the Task Scheduler entry for the daemon (requires elevation for S4U). */
24
25
  private ensureDaemonTask;
25
26
  /** Start the daemon via Task Scheduler (runs outside any session's job object). */
26
27
  private startDaemonTask;
@@ -5,8 +5,6 @@ import { CONFIG_DIR, loadConfig } from "../config.js";
5
5
  import { getTaskDir, readTaskStatus } from "../task.js";
6
6
  const TASK_PREFIX = "\\Palmier\\PalmierTask-";
7
7
  const DAEMON_TASK_NAME = "PalmierDaemon";
8
- const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
9
- const DAEMON_VBS_FILE = path.join(CONFIG_DIR, "daemon.vbs");
10
8
  const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
11
9
  /**
12
10
  * Convert a cron expression or "once" trigger to Task Scheduler XML trigger elements.
@@ -48,13 +46,19 @@ export function triggerToXml(trigger) {
48
46
  /**
49
47
  * Build a complete Task Scheduler XML definition.
50
48
  */
51
- export function buildTaskXml(tr, triggers) {
49
+ export function buildTaskXml(tr, triggers, foreground) {
52
50
  const [command, ...argParts] = tr.match(/"[^"]*"|[^\s]+/g) ?? [];
53
51
  const commandStr = command?.replace(/"/g, "") ?? "";
54
52
  const argsStr = argParts.map((a) => a.replace(/"/g, "")).join(" ");
55
53
  return [
56
54
  `<?xml version="1.0" encoding="UTF-16"?>`,
57
55
  `<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">`,
56
+ ` <Principals>`,
57
+ ` <Principal>`,
58
+ ` <LogonType>${foreground ? "InteractiveToken" : "S4U"}</LogonType>`,
59
+ ` <RunLevel>LeastPrivilege</RunLevel>`,
60
+ ` </Principal>`,
61
+ ` </Principals>`,
58
62
  ` <Settings>`,
59
63
  ` <MultipleInstancesPolicy>StopExisting</MultipleInstancesPolicy>`,
60
64
  ` <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>`,
@@ -77,75 +81,72 @@ function schtasksTaskName(taskId) {
77
81
  export class WindowsPlatform {
78
82
  installDaemon(config) {
79
83
  const script = process.argv[1] || "palmier";
80
- // Create the Task Scheduler entry for the daemon
84
+ // Create the Task Scheduler entry for the daemon (BootTrigger starts it at system boot)
81
85
  this.ensureDaemonTask(script);
82
- // Registry Run key triggers the Task Scheduler entry on logon,
83
- // so the daemon always runs outside any session's job object.
84
- const regValue = `schtasks /run /tn "\\Palmier\\${DAEMON_TASK_NAME}"`;
85
- try {
86
- execFileSync("reg", [
87
- "add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
88
- "/v", DAEMON_TASK_NAME, "/t", "REG_SZ", "/d", regValue, "/f",
89
- ], { encoding: "utf-8", stdio: "pipe" });
90
- console.log(`Registry Run key "${DAEMON_TASK_NAME}" installed (runs at logon).`);
91
- }
92
- catch (err) {
93
- console.error(`Warning: failed to install registry run entry: ${err}`);
94
- console.error("You may need to start palmier serve manually.");
95
- }
96
86
  // Start the daemon now
97
87
  this.startDaemonTask();
98
88
  console.log("\nHost initialization complete!");
99
89
  }
100
- async restartDaemon() {
101
- const oldPid = fs.existsSync(DAEMON_PID_FILE)
102
- ? fs.readFileSync(DAEMON_PID_FILE, "utf-8").trim()
103
- : null;
104
- if (oldPid && oldPid === String(process.pid)) {
105
- // We ARE the old daemon (auto-update) — spawn replacement then exit.
106
- this.startDaemonTask();
107
- process.exit(0);
90
+ uninstallDaemon() {
91
+ const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
92
+ // Stop the daemon via Task Scheduler
93
+ try {
94
+ execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
108
95
  }
109
- // Kill old daemon by PID
110
- if (oldPid) {
111
- try {
112
- execFileSync("taskkill", ["/pid", oldPid, "/f", "/t"], { windowsHide: true, stdio: "pipe" });
113
- }
114
- catch {
115
- // Process may have already exited
116
- }
96
+ catch { /* task may not be running */ }
97
+ // Remove daemon scheduled task (elevated — S4U task requires elevation to delete)
98
+ try {
99
+ execFileSync("powershell", [
100
+ "-Command", `Start-Process -Verb RunAs -Wait -FilePath schtasks -ArgumentList '/delete /tn "${tn}" /f'`,
101
+ ], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
102
+ console.log("Daemon task removed.");
117
103
  }
118
- // Also kill any stale palmier serve processes (e.g. leftover from a previous daemon)
104
+ catch { /* task may not exist */ }
105
+ // Remove all Palmier task timers
119
106
  try {
120
- const out = execFileSync("wmic", ["process", "where", `CommandLine like '%palmier%serve%' and ProcessId != '${process.pid}'`, "get", "ProcessId"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
107
+ const out = execFileSync("schtasks", ["/query", "/fo", "CSV", "/nh"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
121
108
  for (const line of out.split("\n")) {
122
- const pid = line.trim();
123
- if (pid && /^\d+$/.test(pid)) {
109
+ const match = line.match(/"(\\Palmier\\PalmierTask-[^"]+)"/);
110
+ if (match) {
124
111
  try {
125
- execFileSync("taskkill", ["/pid", pid, "/f", "/t"], { windowsHide: true, stdio: "pipe" });
112
+ execFileSync("schtasks", ["/end", "/tn", match[1]], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
126
113
  }
127
- catch { }
114
+ catch { /* ignore */ }
115
+ try {
116
+ execFileSync("schtasks", ["/delete", "/tn", match[1], "/f"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
117
+ }
118
+ catch { /* ignore */ }
128
119
  }
129
120
  }
121
+ console.log("Task timers removed.");
130
122
  }
131
- catch {
132
- // wmic may not be available on all Windows versions
123
+ catch { /* ignore */ }
124
+ console.log("Palmier daemon and tasks uninstalled.");
125
+ }
126
+ async restartDaemon() {
127
+ const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
128
+ // Stop the daemon via Task Scheduler
129
+ try {
130
+ execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
133
131
  }
132
+ catch { /* task may not be running */ }
133
+ // Start it again
134
134
  this.startDaemonTask();
135
135
  }
136
- /** Create or update the Task Scheduler entry for the daemon. */
136
+ /** Create or update the Task Scheduler entry for the daemon (requires elevation for S4U). */
137
137
  ensureDaemonTask(script) {
138
- const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" serve", 0, False`;
139
- fs.writeFileSync(DAEMON_VBS_FILE, vbs, "utf-8");
140
- const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
141
138
  const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
142
- const tr = `"${wscript}" "${DAEMON_VBS_FILE}"`;
143
- const xml = buildTaskXml(tr, [`<TimeTrigger><StartBoundary>2000-01-01T00:00:00</StartBoundary></TimeTrigger>`]);
139
+ const tr = `"${process.execPath}" "${script}" serve`;
140
+ const xml = buildTaskXml(tr, [`<BootTrigger><Enabled>true</Enabled></BootTrigger>`]);
144
141
  const xmlPath = path.join(CONFIG_DIR, "daemon-task.xml");
145
142
  try {
146
143
  const bom = Buffer.from([0xFF, 0xFE]);
147
144
  fs.writeFileSync(xmlPath, Buffer.concat([bom, Buffer.from(xml, "utf16le")]));
148
- execFileSync("schtasks", ["/create", "/tn", tn, "/xml", xmlPath, "/f"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
145
+ // S4U LogonType requires elevation spawn schtasks via RunAs
146
+ const args = `/create /tn "${tn}" /xml "${xmlPath}" /f`;
147
+ execFileSync("powershell", [
148
+ "-Command", `Start-Process -Verb RunAs -Wait -FilePath schtasks -ArgumentList '${args}'`,
149
+ ], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
149
150
  }
150
151
  catch (err) {
151
152
  const e = err;
@@ -174,12 +175,7 @@ export class WindowsPlatform {
174
175
  const taskId = task.frontmatter.id;
175
176
  const tn = schtasksTaskName(taskId);
176
177
  const script = process.argv[1] || "palmier";
177
- // Write a VBS launcher so the task runs without a visible console window
178
- const vbsPath = path.join(CONFIG_DIR, `task-${taskId}.vbs`);
179
- const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" run ${taskId}", 0, True`;
180
- fs.writeFileSync(vbsPath, vbs, "utf-8");
181
- const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
182
- const tr = `"${wscript}" "${vbsPath}"`;
178
+ const tr = `"${process.execPath}" "${script}" run ${taskId}`;
183
179
  // Build trigger XML elements
184
180
  const triggerElements = [];
185
181
  if (task.frontmatter.triggers_enabled) {
@@ -198,7 +194,9 @@ export class WindowsPlatform {
198
194
  }
199
195
  // Write XML and register via schtasks — gives us full control over
200
196
  // settings like MultipleInstancesPolicy that schtasks flags don't expose.
201
- const xml = buildTaskXml(tr, triggerElements);
197
+ // S4U LogonType ensures no console window (unless foreground_mode is set).
198
+ // Works without elevation because the daemon (which calls this) runs elevated.
199
+ const xml = buildTaskXml(tr, triggerElements, task.frontmatter.foreground_mode);
202
200
  const xmlPath = path.join(CONFIG_DIR, `task-${taskId}.xml`);
203
201
  try {
204
202
  // schtasks /xml requires UTF-16LE with BOM
@@ -227,10 +225,6 @@ export class WindowsPlatform {
227
225
  catch {
228
226
  // Task might not exist — that's fine
229
227
  }
230
- try {
231
- fs.unlinkSync(path.join(CONFIG_DIR, `task-${taskId}.vbs`));
232
- }
233
- catch { /* ignore */ }
234
228
  }
235
229
  async startTask(taskId) {
236
230
  const tn = schtasksTaskName(taskId);
@@ -151,6 +151,7 @@ export function createRpcHandler(config, nc) {
151
151
  tasks: tasks.map((task) => flattenTask(task)),
152
152
  agents: config.agents ?? [],
153
153
  version: currentVersion,
154
+ host_platform: process.platform,
154
155
  };
155
156
  }
156
157
  case "task.create": {
@@ -184,6 +185,7 @@ export function createRpcHandler(config, nc) {
184
185
  triggers_enabled: params.triggers_enabled ?? true,
185
186
  requires_confirmation: params.requires_confirmation ?? true,
186
187
  ...(params.yolo_mode ? { yolo_mode: true } : {}),
188
+ ...(params.foreground_mode ? { foreground_mode: true } : {}),
187
189
  ...(params.command ? { command: params.command } : {}),
188
190
  },
189
191
  body,
@@ -217,6 +219,8 @@ export function createRpcHandler(config, nc) {
217
219
  if (params.yolo_mode)
218
220
  delete existing.frontmatter.permissions;
219
221
  }
222
+ if (params.foreground_mode !== undefined)
223
+ existing.frontmatter.foreground_mode = params.foreground_mode || undefined;
220
224
  if (params.command !== undefined) {
221
225
  if (params.command) {
222
226
  existing.frontmatter.command = params.command;
@@ -268,6 +272,7 @@ export function createRpcHandler(config, nc) {
268
272
  triggers_enabled: false,
269
273
  requires_confirmation: params.requires_confirmation ?? false,
270
274
  ...(params.yolo_mode ? { yolo_mode: true } : {}),
275
+ ...(params.foreground_mode ? { foreground_mode: true } : {}),
271
276
  ...(params.command ? { command: params.command } : {}),
272
277
  },
273
278
  body: "",
package/dist/types.d.ts CHANGED
@@ -20,6 +20,7 @@ export interface TaskFrontmatter {
20
20
  triggers_enabled: boolean;
21
21
  requires_confirmation: boolean;
22
22
  yolo_mode?: boolean;
23
+ foreground_mode?: boolean;
23
24
  permissions?: RequiredPermission[];
24
25
  command?: string;
25
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -4,6 +4,7 @@ import { detectAgents } from "../agents/agent.js";
4
4
  import { getPlatform } from "../platform/index.js";
5
5
  import { pairCommand } from "./pair.js";
6
6
  import { detectLanIp } from "../transports/http-transport.js";
7
+ import { listTasks } from "../task.js";
7
8
  import type { HostConfig } from "../types.js";
8
9
 
9
10
  type AskFn = (q: string) => Promise<string>;
@@ -63,6 +64,16 @@ export async function initCommand(): Promise<void> {
63
64
  }
64
65
  console.log(` ${dim("Agents:")} ${agents.map((a) => a.label).join(", ")}\n`);
65
66
 
67
+ // Check for existing tasks to recover
68
+ const existingTasks = listTasks(process.cwd());
69
+ if (existingTasks.length > 0) {
70
+ console.log(` ${dim("Recover tasks:")} ${existingTasks.length} existing task(s) found:`);
71
+ for (const t of existingTasks) {
72
+ console.log(` - ${t.frontmatter.name || t.frontmatter.user_prompt.slice(0, 50)}`);
73
+ }
74
+ console.log();
75
+ }
76
+
66
77
  const confirm = await ask("Proceed? (Y/n): ");
67
78
  if (confirm.trim().toLowerCase() === "n") {
68
79
  console.log("\nSetup cancelled.");
@@ -111,6 +122,9 @@ export async function initCommand(): Promise<void> {
111
122
 
112
123
  getPlatform().installDaemon(config);
113
124
 
125
+ // Task recovery happens in the daemon (palmier serve) on startup,
126
+ // since the daemon runs elevated and can create S4U scheduled tasks.
127
+
114
128
  console.log("\nStarting pairing...");
115
129
  rl.close();
116
130
  await pairCommand();
@@ -5,7 +5,7 @@ import { connectNats } from "../nats-client.js";
5
5
  import { createRpcHandler } from "../rpc-handler.js";
6
6
  import { startNatsTransport } from "../transports/nats-transport.js";
7
7
  import { startHttpTransport } from "../transports/http-transport.js";
8
- import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage } from "../task.js";
8
+ import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage, listTasks } from "../task.js";
9
9
  import { publishHostEvent } from "../events.js";
10
10
  import { getPlatform } from "../platform/index.js";
11
11
  import { detectAgents } from "../agents/agent.js";
@@ -106,6 +106,17 @@ export async function serveCommand(): Promise<void> {
106
106
  // Reconcile any tasks stuck from before daemon started
107
107
  await checkStaleTasks(config, nc);
108
108
 
109
+ // Ensure all tasks have their scheduler entries (recovery after init/reinstall)
110
+ const platform = getPlatform();
111
+ const allTasks = listTasks(config.projectRoot);
112
+ for (const task of allTasks) {
113
+ try {
114
+ platform.installTaskTimer(config, task);
115
+ } catch (err) {
116
+ console.error(`Warning: failed to install timer for task ${task.frontmatter.id}: ${err}`);
117
+ }
118
+ }
119
+
109
120
  // Poll for crashed tasks every 30 seconds
110
121
  setInterval(() => {
111
122
  checkStaleTasks(config, nc).catch((err) => {
@@ -0,0 +1,9 @@
1
+ import { getPlatform } from "../platform/index.js";
2
+
3
+ export async function uninstallCommand(): Promise<void> {
4
+ const platform = getPlatform();
5
+ platform.uninstallDaemon();
6
+
7
+ console.log("\nTo uninstall the package: npm uninstall -g palmier");
8
+ console.log("To also remove configuration and task data, see https://github.com/caihongxu/palmier#uninstalling");
9
+ }
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import { serveCommand } from "./commands/serve.js";
13
13
  import { pairCommand } from "./commands/pair.js";
14
14
  import { restartCommand } from "./commands/restart.js";
15
15
  import { clientsListCommand, clientsRevokeCommand, clientsRevokeAllCommand } from "./commands/clients.js";
16
+ import { uninstallCommand } from "./commands/uninstall.js";
16
17
 
17
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
19
  const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
@@ -93,6 +94,13 @@ clientsCmd
93
94
  await clientsRevokeAllCommand();
94
95
  });
95
96
 
97
+ program
98
+ .command("uninstall")
99
+ .description("Stop the daemon and remove all scheduled tasks")
100
+ .action(async () => {
101
+ await uninstallCommand();
102
+ });
103
+
96
104
  // No subcommand → default to serve
97
105
  if (process.argv.length <= 2) {
98
106
  process.argv.push("serve");
@@ -121,6 +121,32 @@ WantedBy=default.target
121
121
  console.log("\nHost initialization complete!");
122
122
  }
123
123
 
124
+ uninstallDaemon(): void {
125
+ try {
126
+ execSync("systemctl --user stop palmier.service 2>/dev/null", { stdio: "pipe" });
127
+ execSync("systemctl --user disable palmier.service 2>/dev/null", { stdio: "pipe" });
128
+ } catch { /* service may not exist */ }
129
+
130
+ // Remove daemon service file
131
+ const servicePath = path.join(UNIT_DIR, "palmier.service");
132
+ try { fs.unlinkSync(servicePath); } catch { /* ignore */ }
133
+
134
+ // Remove all task timers and services
135
+ try {
136
+ const files = fs.readdirSync(UNIT_DIR).filter((f) => f.startsWith("palmier-task-"));
137
+ for (const f of files) {
138
+ const unit = f.replace(/\.(timer|service)$/, "");
139
+ try { execSync(`systemctl --user stop ${f} 2>/dev/null`, { stdio: "pipe" }); } catch { /* ignore */ }
140
+ try { execSync(`systemctl --user disable ${f} 2>/dev/null`, { stdio: "pipe" }); } catch { /* ignore */ }
141
+ try { fs.unlinkSync(path.join(UNIT_DIR, f)); } catch { /* ignore */ }
142
+ }
143
+ } catch { /* ignore */ }
144
+
145
+ try { execSync("systemctl --user daemon-reload", { stdio: "pipe" }); } catch { /* ignore */ }
146
+
147
+ console.log("Palmier daemon and tasks uninstalled.");
148
+ }
149
+
124
150
  async restartDaemon(): Promise<void> {
125
151
  // If called from a user's terminal, save the current PATH for future use.
126
152
  // If called from the daemon (auto-update), read the saved PATH instead.
@@ -11,6 +11,9 @@ export interface PlatformService {
11
11
  /** Restart the `palmier serve` daemon. */
12
12
  restartDaemon(): Promise<void>;
13
13
 
14
+ /** Stop the daemon and remove all scheduled tasks/timers. */
15
+ uninstallDaemon(): void;
16
+
14
17
  /** Install a scheduled trigger (timer) for a task. */
15
18
  installTaskTimer(config: HostConfig, task: ParsedTask): void;
16
19
 
@@ -9,8 +9,6 @@ import { getTaskDir, readTaskStatus } from "../task.js";
9
9
 
10
10
  const TASK_PREFIX = "\\Palmier\\PalmierTask-";
11
11
  const DAEMON_TASK_NAME = "PalmierDaemon";
12
- const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
13
- const DAEMON_VBS_FILE = path.join(CONFIG_DIR, "daemon.vbs");
14
12
 
15
13
 
16
14
  const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
@@ -60,7 +58,7 @@ export function triggerToXml(trigger: { type: string; value: string }): string {
60
58
  /**
61
59
  * Build a complete Task Scheduler XML definition.
62
60
  */
63
- export function buildTaskXml(tr: string, triggers: string[]): string {
61
+ export function buildTaskXml(tr: string, triggers: string[], foreground?: boolean): string {
64
62
  const [command, ...argParts] = tr.match(/"[^"]*"|[^\s]+/g) ?? [];
65
63
  const commandStr = command?.replace(/"/g, "") ?? "";
66
64
  const argsStr = argParts.map((a) => a.replace(/"/g, "")).join(" ");
@@ -68,6 +66,12 @@ export function buildTaskXml(tr: string, triggers: string[]): string {
68
66
  return [
69
67
  `<?xml version="1.0" encoding="UTF-16"?>`,
70
68
  `<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">`,
69
+ ` <Principals>`,
70
+ ` <Principal>`,
71
+ ` <LogonType>${foreground ? "InteractiveToken" : "S4U"}</LogonType>`,
72
+ ` <RunLevel>LeastPrivilege</RunLevel>`,
73
+ ` </Principal>`,
74
+ ` </Principals>`,
71
75
  ` <Settings>`,
72
76
  ` <MultipleInstancesPolicy>StopExisting</MultipleInstancesPolicy>`,
73
77
  ` <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>`,
@@ -93,85 +97,80 @@ export class WindowsPlatform implements PlatformService {
93
97
  installDaemon(config: HostConfig): void {
94
98
  const script = process.argv[1] || "palmier";
95
99
 
96
- // Create the Task Scheduler entry for the daemon
100
+ // Create the Task Scheduler entry for the daemon (BootTrigger starts it at system boot)
97
101
  this.ensureDaemonTask(script);
98
102
 
99
- // Registry Run key triggers the Task Scheduler entry on logon,
100
- // so the daemon always runs outside any session's job object.
101
- const regValue = `schtasks /run /tn "\\Palmier\\${DAEMON_TASK_NAME}"`;
102
- try {
103
- execFileSync("reg", [
104
- "add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
105
- "/v", DAEMON_TASK_NAME, "/t", "REG_SZ", "/d", regValue, "/f",
106
- ], { encoding: "utf-8", stdio: "pipe" });
107
- console.log(`Registry Run key "${DAEMON_TASK_NAME}" installed (runs at logon).`);
108
- } catch (err) {
109
- console.error(`Warning: failed to install registry run entry: ${err}`);
110
- console.error("You may need to start palmier serve manually.");
111
- }
112
-
113
103
  // Start the daemon now
114
104
  this.startDaemonTask();
115
105
 
116
106
  console.log("\nHost initialization complete!");
117
107
  }
118
108
 
119
- async restartDaemon(): Promise<void> {
120
- const oldPid = fs.existsSync(DAEMON_PID_FILE)
121
- ? fs.readFileSync(DAEMON_PID_FILE, "utf-8").trim()
122
- : null;
123
-
124
- if (oldPid && oldPid === String(process.pid)) {
125
- // We ARE the old daemon (auto-update) — spawn replacement then exit.
126
- this.startDaemonTask();
127
- process.exit(0);
128
- }
109
+ uninstallDaemon(): void {
110
+ const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
129
111
 
130
- // Kill old daemon by PID
131
- if (oldPid) {
132
- try {
133
- execFileSync("taskkill", ["/pid", oldPid, "/f", "/t"], { windowsHide: true, stdio: "pipe" });
134
- } catch {
135
- // Process may have already exited
136
- }
137
- }
112
+ // Stop the daemon via Task Scheduler
113
+ try {
114
+ execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
115
+ } catch { /* task may not be running */ }
116
+
117
+ // Remove daemon scheduled task (elevated — S4U task requires elevation to delete)
118
+ try {
119
+ execFileSync("powershell", [
120
+ "-Command", `Start-Process -Verb RunAs -Wait -FilePath schtasks -ArgumentList '/delete /tn "${tn}" /f'`,
121
+ ], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
122
+ console.log("Daemon task removed.");
123
+ } catch { /* task may not exist */ }
138
124
 
139
- // Also kill any stale palmier serve processes (e.g. leftover from a previous daemon)
125
+ // Remove all Palmier task timers
140
126
  try {
141
- const out = execFileSync("wmic", ["process", "where", `CommandLine like '%palmier%serve%' and ProcessId != '${process.pid}'`, "get", "ProcessId"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
127
+ const out = execFileSync("schtasks", ["/query", "/fo", "CSV", "/nh"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
142
128
  for (const line of out.split("\n")) {
143
- const pid = line.trim();
144
- if (pid && /^\d+$/.test(pid)) {
145
- try { execFileSync("taskkill", ["/pid", pid, "/f", "/t"], { windowsHide: true, stdio: "pipe" }); } catch {}
129
+ const match = line.match(/"(\\Palmier\\PalmierTask-[^"]+)"/);
130
+ if (match) {
131
+ try { execFileSync("schtasks", ["/end", "/tn", match[1]], { encoding: "utf-8", windowsHide: true, stdio: "pipe" }); } catch { /* ignore */ }
132
+ try { execFileSync("schtasks", ["/delete", "/tn", match[1], "/f"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" }); } catch { /* ignore */ }
146
133
  }
147
134
  }
148
- } catch {
149
- // wmic may not be available on all Windows versions
150
- }
135
+ console.log("Task timers removed.");
136
+ } catch { /* ignore */ }
137
+
138
+ console.log("Palmier daemon and tasks uninstalled.");
139
+ }
151
140
 
141
+ async restartDaemon(): Promise<void> {
142
+ const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
143
+
144
+ // Stop the daemon via Task Scheduler
145
+ try {
146
+ execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
147
+ } catch { /* task may not be running */ }
148
+
149
+ // Start it again
152
150
  this.startDaemonTask();
153
151
  }
154
152
 
155
- /** Create or update the Task Scheduler entry for the daemon. */
153
+ /** Create or update the Task Scheduler entry for the daemon (requires elevation for S4U). */
156
154
  private ensureDaemonTask(script: string): void {
157
- const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" serve", 0, False`;
158
- fs.writeFileSync(DAEMON_VBS_FILE, vbs, "utf-8");
159
-
160
- const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
161
155
  const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
162
- const tr = `"${wscript}" "${DAEMON_VBS_FILE}"`;
163
- const xml = buildTaskXml(tr, [`<TimeTrigger><StartBoundary>2000-01-01T00:00:00</StartBoundary></TimeTrigger>`]);
156
+ const tr = `"${process.execPath}" "${script}" serve`;
157
+ const xml = buildTaskXml(tr, [`<BootTrigger><Enabled>true</Enabled></BootTrigger>`]);
164
158
  const xmlPath = path.join(CONFIG_DIR, "daemon-task.xml");
165
159
  try {
166
160
  const bom = Buffer.from([0xFF, 0xFE]);
167
161
  fs.writeFileSync(xmlPath, Buffer.concat([bom, Buffer.from(xml, "utf16le")]));
168
- execFileSync("schtasks", ["/create", "/tn", tn, "/xml", xmlPath, "/f"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
162
+ // S4U LogonType requires elevation spawn schtasks via RunAs
163
+ const args = `/create /tn "${tn}" /xml "${xmlPath}" /f`;
164
+ execFileSync("powershell", [
165
+ "-Command", `Start-Process -Verb RunAs -Wait -FilePath schtasks -ArgumentList '${args}'`,
166
+ ], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
169
167
  } catch (err: unknown) {
170
168
  const e = err as { stderr?: string };
171
169
  console.error(`Failed to create daemon task: ${e.stderr || err}`);
172
170
  } finally {
173
171
  try { fs.unlinkSync(xmlPath); } catch { /* ignore */ }
174
172
  }
173
+
175
174
  }
176
175
 
177
176
  /** Start the daemon via Task Scheduler (runs outside any session's job object). */
@@ -190,14 +189,7 @@ export class WindowsPlatform implements PlatformService {
190
189
  const taskId = task.frontmatter.id;
191
190
  const tn = schtasksTaskName(taskId);
192
191
  const script = process.argv[1] || "palmier";
193
-
194
- // Write a VBS launcher so the task runs without a visible console window
195
- const vbsPath = path.join(CONFIG_DIR, `task-${taskId}.vbs`);
196
- const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" run ${taskId}", 0, True`;
197
- fs.writeFileSync(vbsPath, vbs, "utf-8");
198
-
199
- const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
200
- const tr = `"${wscript}" "${vbsPath}"`;
192
+ const tr = `"${process.execPath}" "${script}" run ${taskId}`;
201
193
 
202
194
  // Build trigger XML elements
203
195
  const triggerElements: string[] = [];
@@ -217,7 +209,9 @@ export class WindowsPlatform implements PlatformService {
217
209
 
218
210
  // Write XML and register via schtasks — gives us full control over
219
211
  // settings like MultipleInstancesPolicy that schtasks flags don't expose.
220
- const xml = buildTaskXml(tr, triggerElements);
212
+ // S4U LogonType ensures no console window (unless foreground_mode is set).
213
+ // Works without elevation because the daemon (which calls this) runs elevated.
214
+ const xml = buildTaskXml(tr, triggerElements, task.frontmatter.foreground_mode);
221
215
  const xmlPath = path.join(CONFIG_DIR, `task-${taskId}.xml`);
222
216
  try {
223
217
  // schtasks /xml requires UTF-16LE with BOM
@@ -232,6 +226,7 @@ export class WindowsPlatform implements PlatformService {
232
226
  } finally {
233
227
  try { fs.unlinkSync(xmlPath); } catch { /* ignore */ }
234
228
  }
229
+
235
230
  }
236
231
 
237
232
  removeTaskTimer(taskId: string): void {
@@ -241,7 +236,6 @@ export class WindowsPlatform implements PlatformService {
241
236
  } catch {
242
237
  // Task might not exist — that's fine
243
238
  }
244
- try { fs.unlinkSync(path.join(CONFIG_DIR, `task-${taskId}.vbs`)); } catch { /* ignore */ }
245
239
  }
246
240
 
247
241
  async startTask(taskId: string): Promise<void> {
@@ -178,6 +178,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
178
178
  tasks: tasks.map((task) => flattenTask(task)),
179
179
  agents: config.agents ?? [],
180
180
  version: currentVersion,
181
+ host_platform: process.platform,
181
182
  };
182
183
  }
183
184
 
@@ -189,6 +190,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
189
190
  triggers_enabled?: boolean;
190
191
  requires_confirmation?: boolean;
191
192
  yolo_mode?: boolean;
193
+ foreground_mode?: boolean;
192
194
  command?: string;
193
195
  };
194
196
 
@@ -220,6 +222,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
220
222
  triggers_enabled: params.triggers_enabled ?? true,
221
223
  requires_confirmation: params.requires_confirmation ?? true,
222
224
  ...(params.yolo_mode ? { yolo_mode: true } : {}),
225
+ ...(params.foreground_mode ? { foreground_mode: true } : {}),
223
226
  ...(params.command ? { command: params.command } : {}),
224
227
  },
225
228
  body,
@@ -241,6 +244,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
241
244
  triggers_enabled?: boolean;
242
245
  requires_confirmation?: boolean;
243
246
  yolo_mode?: boolean;
247
+ foreground_mode?: boolean;
244
248
  command?: string;
245
249
  };
246
250
 
@@ -263,6 +267,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
263
267
  existing.frontmatter.yolo_mode = params.yolo_mode || undefined;
264
268
  if (params.yolo_mode) delete existing.frontmatter.permissions;
265
269
  }
270
+ if (params.foreground_mode !== undefined) existing.frontmatter.foreground_mode = params.foreground_mode || undefined;
266
271
  if (params.command !== undefined) {
267
272
  if (params.command) {
268
273
  existing.frontmatter.command = params.command;
@@ -310,6 +315,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
310
315
  agent: string;
311
316
  requires_confirmation?: boolean;
312
317
  yolo_mode?: boolean;
318
+ foreground_mode?: boolean;
313
319
  command?: string;
314
320
  };
315
321
 
@@ -326,6 +332,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
326
332
  triggers_enabled: false,
327
333
  requires_confirmation: params.requires_confirmation ?? false,
328
334
  ...(params.yolo_mode ? { yolo_mode: true } : {}),
335
+ ...(params.foreground_mode ? { foreground_mode: true } : {}),
329
336
  ...(params.command ? { command: params.command } : {}),
330
337
  },
331
338
  body: "",
package/src/types.ts CHANGED
@@ -24,6 +24,7 @@ export interface TaskFrontmatter {
24
24
  triggers_enabled: boolean;
25
25
  requires_confirmation: boolean;
26
26
  yolo_mode?: boolean;
27
+ foreground_mode?: boolean;
27
28
  permissions?: RequiredPermission[];
28
29
  command?: string;
29
30
  }
@@ -60,6 +60,8 @@ describe("buildTaskXml", () => {
60
60
  const xml = buildTaskXml(tr, triggers);
61
61
 
62
62
  assert.ok(xml.includes('<?xml version="1.0" encoding="UTF-16"?>'), "should have XML declaration");
63
+ assert.ok(xml.includes("<LogonType>S4U</LogonType>"), "should use S4U logon type");
64
+ assert.ok(xml.includes("<RunLevel>LeastPrivilege</RunLevel>"), "should use least privilege");
63
65
  assert.ok(xml.includes("<MultipleInstancesPolicy>StopExisting</MultipleInstancesPolicy>"), "should set StopExisting");
64
66
  assert.ok(xml.includes("<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>"), "should allow on battery");
65
67
  assert.ok(xml.includes("<Command>C:\\Program Files\\nodejs\\node.exe</Command>"), "should extract command");