palmier 0.3.2 → 0.3.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 +14 -17
- package/dist/agents/agent.js +4 -1
- package/dist/agents/copilot.d.ts +8 -0
- package/dist/agents/copilot.js +53 -0
- package/dist/commands/lan.js +1 -1
- package/dist/commands/pair.d.ts +2 -0
- package/dist/commands/pair.js +8 -1
- package/dist/platform/windows.js +84 -82
- package/dist/update-checker.d.ts +2 -0
- package/dist/update-checker.js +7 -2
- package/package.json +1 -1
- package/src/agents/agent.ts +4 -1
- package/src/agents/copilot.ts +55 -0
- package/src/commands/lan.ts +1 -1
- package/src/commands/pair.ts +11 -1
- package/src/platform/windows.ts +84 -82
- package/src/update-checker.ts +7 -2
- package/dist/pairing.d.ts +0 -3
- package/dist/pairing.js +0 -9
- package/src/pairing.ts +0 -10
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**Website:** [palmier.me](https://www.palmier.me) | **App:** [app.palmier.me](https://app.palmier.me)
|
|
8
8
|
|
|
9
|
-
A Node.js CLI that runs on your machine as a persistent daemon. It
|
|
9
|
+
A Node.js CLI that runs on your machine as a persistent daemon. It lets you create, schedule, and run AI agent tasks from your phone or browser, communicating via a cloud relay (NATS) and/or direct HTTP.
|
|
10
10
|
|
|
11
11
|
> **Important:** By using Palmier, you agree to the [Terms of Service](https://www.palmier.me/terms) and [Privacy Policy](https://www.palmier.me/privacy). See the [Disclaimer](#disclaimer) section below.
|
|
12
12
|
|
|
@@ -16,17 +16,17 @@ The host supports two independent connection modes, enabled during `palmier init
|
|
|
16
16
|
|
|
17
17
|
| Mode | Transport | PWA URL | Features |
|
|
18
18
|
|------|-----------|---------|----------|
|
|
19
|
-
| **Server** |
|
|
19
|
+
| **Server** | Cloud relay (NATS) | `https://app.palmier.me` | Push notifications, remote access |
|
|
20
20
|
| **LAN** | HTTP (direct, on-demand) | `http://<host-ip>:7400` | Low-latency, no external server needed |
|
|
21
21
|
|
|
22
|
-
**Server mode** relays communication through the Palmier server via NATS. All features including push notifications are available. The PWA is served over HTTPS.
|
|
22
|
+
**Server mode** relays communication through the Palmier cloud server (via [NATS](https://nats.io), a lightweight messaging system). All features including push notifications are available. The PWA is served over HTTPS.
|
|
23
23
|
|
|
24
24
|
**LAN mode** is started on-demand via `palmier lan`. It runs a local HTTP server that reverse-proxies PWA assets from `app.palmier.me` and serves API endpoints locally. The browser accesses everything at `http://<host-ip>:<port>` (same-origin). Push notifications are not available in LAN mode.
|
|
25
25
|
|
|
26
26
|
## Prerequisites
|
|
27
27
|
|
|
28
28
|
- **Node.js 24+**
|
|
29
|
-
- An agent CLI tool for task execution (e.g., Claude Code, Gemini CLI, OpenAI Codex)
|
|
29
|
+
- An agent CLI tool for task execution (e.g., Claude Code, Gemini CLI, OpenAI Codex, GitHub Copilot)
|
|
30
30
|
- **Linux with systemd** or **Windows 10/11**
|
|
31
31
|
|
|
32
32
|
## Installation
|
|
@@ -35,6 +35,8 @@ The host supports two independent connection modes, enabled during `palmier init
|
|
|
35
35
|
npm install -g palmier
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
All `palmier` commands should be run from a dedicated Palmier root directory (e.g., `~/palmier`). This is where tasks, configuration, and execution data are stored.
|
|
39
|
+
|
|
38
40
|
## CLI Commands
|
|
39
41
|
|
|
40
42
|
| Command | Description |
|
|
@@ -57,7 +59,7 @@ npm install -g palmier
|
|
|
57
59
|
### Quick Start
|
|
58
60
|
|
|
59
61
|
1. Install the host: `npm install -g palmier`
|
|
60
|
-
2. Run `palmier init` in your
|
|
62
|
+
2. Run `palmier init` in your Palmier root directory (e.g., `~/palmier`).
|
|
61
63
|
3. The wizard detects installed agents, registers with the Palmier server, installs a background daemon, and generates a pairing code.
|
|
62
64
|
4. Enter the pairing code in the Palmier PWA to connect your device.
|
|
63
65
|
|
|
@@ -81,7 +83,7 @@ palmier sessions revoke-all
|
|
|
81
83
|
```
|
|
82
84
|
|
|
83
85
|
The `init` command:
|
|
84
|
-
- Detects installed agent CLIs (Claude Code, Gemini CLI, Codex CLI) and caches the result
|
|
86
|
+
- Detects installed agent CLIs (Claude Code, Gemini CLI, Codex CLI, GitHub Copilot) and caches the result
|
|
85
87
|
- Saves host configuration to `~/.config/palmier/host.json`
|
|
86
88
|
- Installs a background daemon (systemd user service on Linux, Registry Run key on Windows)
|
|
87
89
|
- Auto-enters pair mode to connect your first device
|
|
@@ -153,7 +155,6 @@ src/
|
|
|
153
155
|
spawn-command.ts # Shared helper for spawning CLI tools
|
|
154
156
|
task.ts # Task file management
|
|
155
157
|
types.ts # Shared type definitions
|
|
156
|
-
pairing.ts # OTP code generation and expiry constant
|
|
157
158
|
lan-lock.ts # LAN lockfile path and port reader
|
|
158
159
|
events.ts # Event broadcasting (NATS pub/sub or HTTP SSE)
|
|
159
160
|
agents/
|
|
@@ -161,6 +162,7 @@ src/
|
|
|
161
162
|
claude.ts # Claude Code agent implementation
|
|
162
163
|
gemini.ts # Gemini CLI agent implementation
|
|
163
164
|
codex.ts # Codex CLI agent implementation
|
|
165
|
+
copilot.ts # GitHub Copilot agent implementation
|
|
164
166
|
openclaw.ts # OpenClaw agent implementation
|
|
165
167
|
commands/
|
|
166
168
|
init.ts # Interactive setup wizard (auto-pair)
|
|
@@ -210,11 +212,11 @@ Requires a provisioned host (`palmier init`) with server mode enabled.
|
|
|
210
212
|
|---|---|---|
|
|
211
213
|
| `send-push-notification` | `title`, `body` (required) | Send a push notification to all paired devices |
|
|
212
214
|
|
|
213
|
-
##
|
|
215
|
+
## Uninstalling
|
|
214
216
|
|
|
215
|
-
To fully remove
|
|
217
|
+
To fully remove Palmier from a machine:
|
|
216
218
|
|
|
217
|
-
1. **Unpair
|
|
219
|
+
1. **Unpair your device** in the PWA (via the host menu).
|
|
218
220
|
|
|
219
221
|
2. **Stop and remove the daemon:**
|
|
220
222
|
|
|
@@ -248,16 +250,11 @@ To fully remove a host from a machine:
|
|
|
248
250
|
schtasks /delete /tn "PalmierTask-*" /f 2>$null
|
|
249
251
|
```
|
|
250
252
|
|
|
251
|
-
4. **Remove
|
|
253
|
+
4. **Remove configuration and task data:**
|
|
252
254
|
|
|
253
255
|
```bash
|
|
254
256
|
rm -rf ~/.config/palmier
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
5. **Remove the tasks directory** from your project root:
|
|
258
|
-
|
|
259
|
-
```bash
|
|
260
|
-
rm -rf tasks/
|
|
257
|
+
rm -rf tasks/ # from your Palmier root directory
|
|
261
258
|
```
|
|
262
259
|
|
|
263
260
|
## Disclaimer
|
package/dist/agents/agent.js
CHANGED
|
@@ -2,17 +2,20 @@ import { ClaudeAgent } from "./claude.js";
|
|
|
2
2
|
import { GeminiAgent } from "./gemini.js";
|
|
3
3
|
import { CodexAgent } from "./codex.js";
|
|
4
4
|
import { OpenClawAgent } from "./openclaw.js";
|
|
5
|
+
import { CopilotAgent } from "./copilot.js";
|
|
5
6
|
const agentRegistry = {
|
|
6
7
|
claude: new ClaudeAgent(),
|
|
7
8
|
gemini: new GeminiAgent(),
|
|
8
9
|
codex: new CodexAgent(),
|
|
9
10
|
openclaw: new OpenClawAgent(),
|
|
11
|
+
copilot: new CopilotAgent(),
|
|
10
12
|
};
|
|
11
13
|
const agentLabels = {
|
|
12
14
|
claude: "Claude Code",
|
|
13
15
|
gemini: "Gemini CLI",
|
|
14
16
|
codex: "Codex CLI",
|
|
15
|
-
openclaw: "OpenClaw"
|
|
17
|
+
openclaw: "OpenClaw",
|
|
18
|
+
copilot: "GitHub Copilot",
|
|
16
19
|
};
|
|
17
20
|
export async function detectAgents() {
|
|
18
21
|
const detected = [];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
3
|
+
export declare class CopilotAgent implements AgentTool {
|
|
4
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine;
|
|
5
|
+
getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
|
|
6
|
+
init(): Promise<boolean>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=copilot.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
|
|
6
|
+
import { SHELL } from "../platform/index.js";
|
|
7
|
+
export class CopilotAgent {
|
|
8
|
+
getPlanGenerationCommandLine(prompt) {
|
|
9
|
+
return {
|
|
10
|
+
command: "copilot",
|
|
11
|
+
args: ["-p", prompt],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
getTaskRunCommandLine(task, retryPrompt, extraPermissions) {
|
|
15
|
+
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
16
|
+
const args = ["-p", prompt];
|
|
17
|
+
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
18
|
+
if (allPerms.length > 0) {
|
|
19
|
+
args.push("--allow-tool", allPerms.map((p) => p.name).join(","));
|
|
20
|
+
}
|
|
21
|
+
if (retryPrompt) {
|
|
22
|
+
args.push("--continue");
|
|
23
|
+
}
|
|
24
|
+
return { command: "copilot", args };
|
|
25
|
+
}
|
|
26
|
+
async init() {
|
|
27
|
+
try {
|
|
28
|
+
execSync("gh copilot -v", { stdio: "ignore", shell: SHELL });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
// Register Palmier MCP server in ~/.copilot/mcp-config.json
|
|
34
|
+
try {
|
|
35
|
+
const configDir = path.join(homedir(), ".copilot");
|
|
36
|
+
const configFile = path.join(configDir, "mcp-config.json");
|
|
37
|
+
let config = {};
|
|
38
|
+
if (fs.existsSync(configFile)) {
|
|
39
|
+
config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
40
|
+
}
|
|
41
|
+
const servers = (config.mcpServers ?? {});
|
|
42
|
+
servers.palmier = { command: "palmier", args: ["mcpserver"] };
|
|
43
|
+
config.mcpServers = servers;
|
|
44
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
45
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), "utf-8");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// MCP registration is best-effort
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=copilot.js.map
|
package/dist/commands/lan.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import { loadConfig, CONFIG_DIR } from "../config.js";
|
|
3
3
|
import { createRpcHandler } from "../rpc-handler.js";
|
|
4
4
|
import { startHttpTransport, detectLanIp } from "../transports/http-transport.js";
|
|
5
|
-
import { generatePairingCode } from "
|
|
5
|
+
import { generatePairingCode } from "./pair.js";
|
|
6
6
|
import { LAN_LOCKFILE } from "../lan-lock.js";
|
|
7
7
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
8
8
|
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
package/dist/commands/pair.d.ts
CHANGED
package/dist/commands/pair.js
CHANGED
|
@@ -3,8 +3,15 @@ import { StringCodec } from "nats";
|
|
|
3
3
|
import { loadConfig } from "../config.js";
|
|
4
4
|
import { connectNats } from "../nats-client.js";
|
|
5
5
|
import { addSession } from "../session-store.js";
|
|
6
|
-
import { generatePairingCode, PAIRING_EXPIRY_MS } from "../pairing.js";
|
|
7
6
|
import { getLanPort } from "../lan-lock.js";
|
|
7
|
+
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
|
|
8
|
+
const CODE_LENGTH = 6;
|
|
9
|
+
export const PAIRING_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
|
|
10
|
+
export function generatePairingCode() {
|
|
11
|
+
const bytes = new Uint8Array(CODE_LENGTH);
|
|
12
|
+
crypto.getRandomValues(bytes);
|
|
13
|
+
return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
|
|
14
|
+
}
|
|
8
15
|
function buildPairResponse(config, label) {
|
|
9
16
|
const session = addSession(label);
|
|
10
17
|
return {
|
package/dist/platform/windows.js
CHANGED
|
@@ -15,47 +15,69 @@ function schtasksTr(...subcommand) {
|
|
|
15
15
|
const script = process.argv[1] || "palmier";
|
|
16
16
|
return `"${process.execPath}" "${script}" ${subcommand.join(" ")}`;
|
|
17
17
|
}
|
|
18
|
+
const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
18
19
|
/**
|
|
19
|
-
* Convert
|
|
20
|
+
* Convert a cron expression or "once" trigger to Task Scheduler XML trigger elements.
|
|
20
21
|
*
|
|
21
|
-
* Only these patterns (produced by the PWA UI) are handled:
|
|
22
|
-
* hourly: "0 * * * *"
|
|
23
|
-
* daily: "MM HH * * *"
|
|
24
|
-
* weekly: "MM HH * * D"
|
|
25
|
-
* monthly: "MM HH D * *"
|
|
26
|
-
*
|
|
27
|
-
* Arbitrary cron expressions (ranges, lists, step values) are NOT handled
|
|
28
|
-
* because the UI never generates them.
|
|
22
|
+
* Only these cron patterns (produced by the PWA UI) are handled:
|
|
23
|
+
* hourly: "0 * * * *"
|
|
24
|
+
* daily: "MM HH * * *"
|
|
25
|
+
* weekly: "MM HH * * D"
|
|
26
|
+
* monthly: "MM HH D * *"
|
|
29
27
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
function triggerToXml(trigger) {
|
|
29
|
+
if (trigger.type === "once") {
|
|
30
|
+
// ISO datetime "2026-03-28T09:00"
|
|
31
|
+
return `<TimeTrigger><StartBoundary>${trigger.value}:00</StartBoundary></TimeTrigger>`;
|
|
34
32
|
}
|
|
33
|
+
const parts = trigger.value.trim().split(/\s+/);
|
|
34
|
+
if (parts.length !== 5)
|
|
35
|
+
throw new Error(`Invalid cron expression: ${trigger.value}`);
|
|
35
36
|
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Hourly: "0 * * * *"
|
|
43
|
-
if (hour === "*" && dayOfMonth === "*" && dayOfWeek === "*") {
|
|
44
|
-
return ["/sc", "HOURLY"];
|
|
37
|
+
const st = `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:00`;
|
|
38
|
+
// StartBoundary needs a full date; use a past date as the anchor
|
|
39
|
+
const base = `2000-01-01T${st}`;
|
|
40
|
+
// Hourly
|
|
41
|
+
if (hour === "*") {
|
|
42
|
+
return `<TimeTrigger><StartBoundary>${base}</StartBoundary><Repetition><Interval>PT1H</Interval></Repetition></TimeTrigger>`;
|
|
45
43
|
}
|
|
46
|
-
// Weekly
|
|
44
|
+
// Weekly
|
|
47
45
|
if (dayOfMonth === "*" && dayOfWeek !== "*") {
|
|
48
|
-
const day =
|
|
49
|
-
|
|
50
|
-
throw new Error(`Unsupported day-of-week: ${dayOfWeek}`);
|
|
51
|
-
return ["/sc", "WEEKLY", "/d", day, "/st", st];
|
|
46
|
+
const day = DOW_NAMES[Number(dayOfWeek)] ?? "Monday";
|
|
47
|
+
return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByWeek><DaysOfWeek><${day} /></DaysOfWeek><WeeksInterval>1</WeeksInterval></ScheduleByWeek></CalendarTrigger>`;
|
|
52
48
|
}
|
|
53
|
-
// Monthly
|
|
49
|
+
// Monthly
|
|
54
50
|
if (dayOfMonth !== "*" && dayOfWeek === "*") {
|
|
55
|
-
return
|
|
51
|
+
return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByMonth><DaysOfMonth><Day>${dayOfMonth}</Day></DaysOfMonth><Months><January /><February /><March /><April /><May /><June /><July /><August /><September /><October /><November /><December /></Months></ScheduleByMonth></CalendarTrigger>`;
|
|
56
52
|
}
|
|
57
|
-
// Daily
|
|
58
|
-
return
|
|
53
|
+
// Daily
|
|
54
|
+
return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByDay><DaysInterval>1</DaysInterval></ScheduleByDay></CalendarTrigger>`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build a complete Task Scheduler XML definition.
|
|
58
|
+
*/
|
|
59
|
+
function buildTaskXml(tr, triggers) {
|
|
60
|
+
const [command, ...argParts] = tr.match(/"[^"]*"|[^\s]+/g) ?? [];
|
|
61
|
+
const commandStr = command?.replace(/"/g, "") ?? "";
|
|
62
|
+
const argsStr = argParts.map((a) => a.replace(/"/g, "")).join(" ");
|
|
63
|
+
return [
|
|
64
|
+
`<?xml version="1.0" encoding="UTF-16"?>`,
|
|
65
|
+
`<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">`,
|
|
66
|
+
` <Settings>`,
|
|
67
|
+
` <MultipleInstancesPolicy>StopExisting</MultipleInstancesPolicy>`,
|
|
68
|
+
` <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>`,
|
|
69
|
+
` <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>`,
|
|
70
|
+
` <UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>`,
|
|
71
|
+
` </Settings>`,
|
|
72
|
+
` <Triggers>${triggers.join("")}</Triggers>`,
|
|
73
|
+
` <Actions>`,
|
|
74
|
+
` <Exec>`,
|
|
75
|
+
` <Command>${commandStr}</Command>`,
|
|
76
|
+
` <Arguments>${argsStr}</Arguments>`,
|
|
77
|
+
` </Exec>`,
|
|
78
|
+
` </Actions>`,
|
|
79
|
+
`</Task>`,
|
|
80
|
+
].join("\n");
|
|
59
81
|
}
|
|
60
82
|
function schtasksTaskName(taskId) {
|
|
61
83
|
return `${TASK_PREFIX}${taskId}`;
|
|
@@ -63,8 +85,9 @@ function schtasksTaskName(taskId) {
|
|
|
63
85
|
export class WindowsPlatform {
|
|
64
86
|
installDaemon(config) {
|
|
65
87
|
const script = process.argv[1] || "palmier";
|
|
66
|
-
// Write a VBS launcher that starts the daemon with no visible console window
|
|
67
|
-
|
|
88
|
+
// Write a VBS launcher that starts the daemon with no visible console window.
|
|
89
|
+
// VBS doesn't use backslash escaping — only quotes need doubling ("").
|
|
90
|
+
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" serve", 0, False`;
|
|
68
91
|
fs.writeFileSync(DAEMON_VBS_FILE, vbs, "utf-8");
|
|
69
92
|
const regValue = `"${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe" "${DAEMON_VBS_FILE}"`;
|
|
70
93
|
try {
|
|
@@ -112,64 +135,43 @@ export class WindowsPlatform {
|
|
|
112
135
|
const taskId = task.frontmatter.id;
|
|
113
136
|
const tn = schtasksTaskName(taskId);
|
|
114
137
|
const tr = schtasksTr("run", taskId);
|
|
115
|
-
//
|
|
116
|
-
|
|
138
|
+
// Build trigger XML elements
|
|
139
|
+
const triggerElements = [];
|
|
140
|
+
if (task.frontmatter.triggers_enabled) {
|
|
141
|
+
for (const trigger of task.frontmatter.triggers ?? []) {
|
|
142
|
+
try {
|
|
143
|
+
triggerElements.push(triggerToXml(trigger));
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
console.error(`Invalid trigger: ${err}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Always include a dummy trigger so startTask (/run) works
|
|
151
|
+
if (triggerElements.length === 0) {
|
|
152
|
+
triggerElements.push(`<TimeTrigger><StartBoundary>2000-01-01T00:00:00</StartBoundary></TimeTrigger>`);
|
|
153
|
+
}
|
|
154
|
+
// Write XML and register via schtasks — gives us full control over
|
|
155
|
+
// settings like MultipleInstancesPolicy that schtasks flags don't expose.
|
|
156
|
+
const xml = buildTaskXml(tr, triggerElements);
|
|
157
|
+
const xmlPath = path.join(CONFIG_DIR, `task-${taskId}.xml`);
|
|
117
158
|
try {
|
|
159
|
+
// schtasks /xml requires UTF-16LE with BOM
|
|
160
|
+
const bom = Buffer.from([0xFF, 0xFE]);
|
|
161
|
+
fs.writeFileSync(xmlPath, Buffer.concat([bom, Buffer.from(xml, "utf16le")]));
|
|
118
162
|
execFileSync("schtasks", [
|
|
119
|
-
"/create", "/tn", tn,
|
|
120
|
-
"/tr", tr,
|
|
121
|
-
"/sc", "ONCE", "/sd", "01/01/2000", "/st", "00:00",
|
|
122
|
-
"/f",
|
|
163
|
+
"/create", "/tn", tn, "/xml", xmlPath, "/f",
|
|
123
164
|
], { encoding: "utf-8", windowsHide: true });
|
|
124
165
|
}
|
|
125
166
|
catch (err) {
|
|
126
167
|
const e = err;
|
|
127
168
|
console.error(`Failed to create scheduled task ${tn}: ${e.stderr || err}`);
|
|
128
169
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const triggers = task.frontmatter.triggers || [];
|
|
133
|
-
for (const trigger of triggers) {
|
|
134
|
-
if (trigger.type === "cron") {
|
|
135
|
-
const schedArgs = cronToSchtasksArgs(trigger.value);
|
|
136
|
-
try {
|
|
137
|
-
execFileSync("schtasks", [
|
|
138
|
-
"/create", "/tn", tn,
|
|
139
|
-
"/tr", tr,
|
|
140
|
-
...schedArgs,
|
|
141
|
-
"/f",
|
|
142
|
-
], { encoding: "utf-8", windowsHide: true });
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
const e = err;
|
|
146
|
-
console.error(`Failed to create scheduled task ${tn}: ${e.stderr || err}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
else if (trigger.type === "once") {
|
|
150
|
-
// "once" triggers use ISO datetime: "2026-03-28T09:00"
|
|
151
|
-
const [datePart, timePart] = trigger.value.split("T");
|
|
152
|
-
if (!datePart || !timePart) {
|
|
153
|
-
console.error(`Invalid once trigger value: ${trigger.value}`);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
// schtasks expects MM/DD/YYYY date format
|
|
157
|
-
const [year, month, day] = datePart.split("-");
|
|
158
|
-
const sd = `${month}/${day}/${year}`;
|
|
159
|
-
const st = timePart.slice(0, 5);
|
|
160
|
-
try {
|
|
161
|
-
execFileSync("schtasks", [
|
|
162
|
-
"/create", "/tn", tn,
|
|
163
|
-
"/tr", tr,
|
|
164
|
-
"/sc", "ONCE", "/sd", sd, "/st", st,
|
|
165
|
-
"/f",
|
|
166
|
-
], { encoding: "utf-8", windowsHide: true });
|
|
167
|
-
}
|
|
168
|
-
catch (err) {
|
|
169
|
-
const e = err;
|
|
170
|
-
console.error(`Failed to create once task ${tn}: ${e.stderr || err}`);
|
|
171
|
-
}
|
|
170
|
+
finally {
|
|
171
|
+
try {
|
|
172
|
+
fs.unlinkSync(xmlPath);
|
|
172
173
|
}
|
|
174
|
+
catch { /* ignore */ }
|
|
173
175
|
}
|
|
174
176
|
}
|
|
175
177
|
removeTaskTimer(taskId) {
|
package/dist/update-checker.d.ts
CHANGED
package/dist/update-checker.js
CHANGED
|
@@ -4,8 +4,11 @@ import { fileURLToPath } from "url";
|
|
|
4
4
|
import { spawnCommand } from "./spawn-command.js";
|
|
5
5
|
import { getPlatform } from "./platform/index.js";
|
|
6
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const packageRoot = path.join(__dirname, "..");
|
|
8
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8"));
|
|
9
|
+
/** True when running from a source checkout (has .git) rather than a global npm install. */
|
|
10
|
+
export const isDevBuild = fs.existsSync(path.join(packageRoot, ".git"));
|
|
11
|
+
export const currentVersion = isDevBuild ? `${pkg.version}-dev` : pkg.version;
|
|
9
12
|
let latestVersion = null;
|
|
10
13
|
let lastCheckTime = 0;
|
|
11
14
|
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -13,6 +16,8 @@ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
|
13
16
|
* Check the npm registry for the latest version of palmier.
|
|
14
17
|
*/
|
|
15
18
|
export async function checkForUpdate() {
|
|
19
|
+
if (isDevBuild)
|
|
20
|
+
return;
|
|
16
21
|
const now = Date.now();
|
|
17
22
|
if (now - lastCheckTime < CHECK_INTERVAL_MS)
|
|
18
23
|
return;
|
package/package.json
CHANGED
package/src/agents/agent.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ClaudeAgent } from "./claude.js";
|
|
|
3
3
|
import { GeminiAgent } from "./gemini.js";
|
|
4
4
|
import { CodexAgent } from "./codex.js";
|
|
5
5
|
import { OpenClawAgent } from "./openclaw.js";
|
|
6
|
+
import { CopilotAgent } from "./copilot.js";
|
|
6
7
|
|
|
7
8
|
export interface CommandLine {
|
|
8
9
|
command: string;
|
|
@@ -34,13 +35,15 @@ const agentRegistry: Record<string, AgentTool> = {
|
|
|
34
35
|
gemini: new GeminiAgent(),
|
|
35
36
|
codex: new CodexAgent(),
|
|
36
37
|
openclaw: new OpenClawAgent(),
|
|
38
|
+
copilot: new CopilotAgent(),
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
const agentLabels: Record<string, string> = {
|
|
40
42
|
claude: "Claude Code",
|
|
41
43
|
gemini: "Gemini CLI",
|
|
42
44
|
codex: "Codex CLI",
|
|
43
|
-
openclaw: "OpenClaw"
|
|
45
|
+
openclaw: "OpenClaw",
|
|
46
|
+
copilot: "GitHub Copilot",
|
|
44
47
|
};
|
|
45
48
|
|
|
46
49
|
export interface DetectedAgent {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
7
|
+
import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
|
|
8
|
+
import { SHELL } from "../platform/index.js";
|
|
9
|
+
|
|
10
|
+
export class CopilotAgent implements AgentTool {
|
|
11
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
12
|
+
return {
|
|
13
|
+
command: "copilot",
|
|
14
|
+
args: ["-p", prompt],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
19
|
+
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
20
|
+
const args = ["-p", prompt];
|
|
21
|
+
|
|
22
|
+
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
23
|
+
if (allPerms.length > 0) {
|
|
24
|
+
args.push("--allow-tool", allPerms.map((p) => p.name).join(","));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (retryPrompt) { args.push("--continue"); }
|
|
28
|
+
return { command: "copilot", args};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async init(): Promise<boolean> {
|
|
32
|
+
try {
|
|
33
|
+
execSync("gh copilot -v", { stdio: "ignore", shell: SHELL });
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
// Register Palmier MCP server in ~/.copilot/mcp-config.json
|
|
38
|
+
try {
|
|
39
|
+
const configDir = path.join(homedir(), ".copilot");
|
|
40
|
+
const configFile = path.join(configDir, "mcp-config.json");
|
|
41
|
+
let config: Record<string, unknown> = {};
|
|
42
|
+
if (fs.existsSync(configFile)) {
|
|
43
|
+
config = JSON.parse(fs.readFileSync(configFile, "utf-8")) as Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
const servers = (config.mcpServers ?? {}) as Record<string, unknown>;
|
|
46
|
+
servers.palmier = { command: "palmier", args: ["mcpserver"] };
|
|
47
|
+
config.mcpServers = servers;
|
|
48
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
49
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), "utf-8");
|
|
50
|
+
} catch {
|
|
51
|
+
// MCP registration is best-effort
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/commands/lan.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import { loadConfig, CONFIG_DIR } from "../config.js";
|
|
3
3
|
import { createRpcHandler } from "../rpc-handler.js";
|
|
4
4
|
import { startHttpTransport, detectLanIp } from "../transports/http-transport.js";
|
|
5
|
-
import { generatePairingCode } from "
|
|
5
|
+
import { generatePairingCode } from "./pair.js";
|
|
6
6
|
import { LAN_LOCKFILE } from "../lan-lock.js";
|
|
7
7
|
|
|
8
8
|
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
package/src/commands/pair.ts
CHANGED
|
@@ -3,10 +3,20 @@ import { StringCodec } from "nats";
|
|
|
3
3
|
import { loadConfig } from "../config.js";
|
|
4
4
|
import { connectNats } from "../nats-client.js";
|
|
5
5
|
import { addSession } from "../session-store.js";
|
|
6
|
-
import { generatePairingCode, PAIRING_EXPIRY_MS } from "../pairing.js";
|
|
7
6
|
import { getLanPort } from "../lan-lock.js";
|
|
8
7
|
import type { HostConfig } from "../types.js";
|
|
9
8
|
|
|
9
|
+
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
|
|
10
|
+
const CODE_LENGTH = 6;
|
|
11
|
+
|
|
12
|
+
export const PAIRING_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
|
|
13
|
+
|
|
14
|
+
export function generatePairingCode(): string {
|
|
15
|
+
const bytes = new Uint8Array(CODE_LENGTH);
|
|
16
|
+
crypto.getRandomValues(bytes);
|
|
17
|
+
return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
function buildPairResponse(config: HostConfig, label?: string) {
|
|
11
21
|
const session = addSession(label);
|
|
12
22
|
return {
|
package/src/platform/windows.ts
CHANGED
|
@@ -21,53 +21,76 @@ function schtasksTr(...subcommand: string[]): string {
|
|
|
21
21
|
return `"${process.execPath}" "${script}" ${subcommand.join(" ")}`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
25
|
+
|
|
24
26
|
/**
|
|
25
|
-
* Convert
|
|
26
|
-
*
|
|
27
|
-
* Only these patterns (produced by the PWA UI) are handled:
|
|
28
|
-
* hourly: "0 * * * *" → /sc HOURLY
|
|
29
|
-
* daily: "MM HH * * *" → /sc DAILY /st HH:MM
|
|
30
|
-
* weekly: "MM HH * * D" → /sc WEEKLY /d <day> /st HH:MM
|
|
31
|
-
* monthly: "MM HH D * *" → /sc MONTHLY /d D /st HH:MM
|
|
27
|
+
* Convert a cron expression or "once" trigger to Task Scheduler XML trigger elements.
|
|
32
28
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
29
|
+
* Only these cron patterns (produced by the PWA UI) are handled:
|
|
30
|
+
* hourly: "0 * * * *"
|
|
31
|
+
* daily: "MM HH * * *"
|
|
32
|
+
* weekly: "MM HH * * D"
|
|
33
|
+
* monthly: "MM HH D * *"
|
|
35
34
|
*/
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
function triggerToXml(trigger: { type: string; value: string }): string {
|
|
36
|
+
if (trigger.type === "once") {
|
|
37
|
+
// ISO datetime "2026-03-28T09:00"
|
|
38
|
+
return `<TimeTrigger><StartBoundary>${trigger.value}:00</StartBoundary></TimeTrigger>`;
|
|
40
39
|
}
|
|
41
40
|
|
|
41
|
+
const parts = trigger.value.trim().split(/\s+/);
|
|
42
|
+
if (parts.length !== 5) throw new Error(`Invalid cron expression: ${trigger.value}`);
|
|
42
43
|
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
|
|
44
|
+
const st = `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:00`;
|
|
45
|
+
// StartBoundary needs a full date; use a past date as the anchor
|
|
46
|
+
const base = `2000-01-01T${st}`;
|
|
43
47
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"4": "THU", "5": "FRI", "6": "SAT", "7": "SUN",
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const st = `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}`;
|
|
51
|
-
|
|
52
|
-
// Hourly: "0 * * * *"
|
|
53
|
-
if (hour === "*" && dayOfMonth === "*" && dayOfWeek === "*") {
|
|
54
|
-
return ["/sc", "HOURLY"];
|
|
48
|
+
// Hourly
|
|
49
|
+
if (hour === "*") {
|
|
50
|
+
return `<TimeTrigger><StartBoundary>${base}</StartBoundary><Repetition><Interval>PT1H</Interval></Repetition></TimeTrigger>`;
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
// Weekly
|
|
53
|
+
// Weekly
|
|
58
54
|
if (dayOfMonth === "*" && dayOfWeek !== "*") {
|
|
59
|
-
const day =
|
|
60
|
-
|
|
61
|
-
return ["/sc", "WEEKLY", "/d", day, "/st", st];
|
|
55
|
+
const day = DOW_NAMES[Number(dayOfWeek)] ?? "Monday";
|
|
56
|
+
return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByWeek><DaysOfWeek><${day} /></DaysOfWeek><WeeksInterval>1</WeeksInterval></ScheduleByWeek></CalendarTrigger>`;
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
// Monthly
|
|
59
|
+
// Monthly
|
|
65
60
|
if (dayOfMonth !== "*" && dayOfWeek === "*") {
|
|
66
|
-
return
|
|
61
|
+
return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByMonth><DaysOfMonth><Day>${dayOfMonth}</Day></DaysOfMonth><Months><January /><February /><March /><April /><May /><June /><July /><August /><September /><October /><November /><December /></Months></ScheduleByMonth></CalendarTrigger>`;
|
|
67
62
|
}
|
|
68
63
|
|
|
69
|
-
// Daily
|
|
70
|
-
return
|
|
64
|
+
// Daily
|
|
65
|
+
return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByDay><DaysInterval>1</DaysInterval></ScheduleByDay></CalendarTrigger>`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build a complete Task Scheduler XML definition.
|
|
70
|
+
*/
|
|
71
|
+
function buildTaskXml(tr: string, triggers: string[]): string {
|
|
72
|
+
const [command, ...argParts] = tr.match(/"[^"]*"|[^\s]+/g) ?? [];
|
|
73
|
+
const commandStr = command?.replace(/"/g, "") ?? "";
|
|
74
|
+
const argsStr = argParts.map((a) => a.replace(/"/g, "")).join(" ");
|
|
75
|
+
|
|
76
|
+
return [
|
|
77
|
+
`<?xml version="1.0" encoding="UTF-16"?>`,
|
|
78
|
+
`<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">`,
|
|
79
|
+
` <Settings>`,
|
|
80
|
+
` <MultipleInstancesPolicy>StopExisting</MultipleInstancesPolicy>`,
|
|
81
|
+
` <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>`,
|
|
82
|
+
` <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>`,
|
|
83
|
+
` <UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>`,
|
|
84
|
+
` </Settings>`,
|
|
85
|
+
` <Triggers>${triggers.join("")}</Triggers>`,
|
|
86
|
+
` <Actions>`,
|
|
87
|
+
` <Exec>`,
|
|
88
|
+
` <Command>${commandStr}</Command>`,
|
|
89
|
+
` <Arguments>${argsStr}</Arguments>`,
|
|
90
|
+
` </Exec>`,
|
|
91
|
+
` </Actions>`,
|
|
92
|
+
`</Task>`,
|
|
93
|
+
].join("\n");
|
|
71
94
|
}
|
|
72
95
|
|
|
73
96
|
function schtasksTaskName(taskId: string): string {
|
|
@@ -78,8 +101,9 @@ export class WindowsPlatform implements PlatformService {
|
|
|
78
101
|
installDaemon(config: HostConfig): void {
|
|
79
102
|
const script = process.argv[1] || "palmier";
|
|
80
103
|
|
|
81
|
-
// Write a VBS launcher that starts the daemon with no visible console window
|
|
82
|
-
|
|
104
|
+
// Write a VBS launcher that starts the daemon with no visible console window.
|
|
105
|
+
// VBS doesn't use backslash escaping — only quotes need doubling ("").
|
|
106
|
+
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" serve", 0, False`;
|
|
83
107
|
fs.writeFileSync(DAEMON_VBS_FILE, vbs, "utf-8");
|
|
84
108
|
|
|
85
109
|
const regValue = `"${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe" "${DAEMON_VBS_FILE}"`;
|
|
@@ -134,60 +158,38 @@ export class WindowsPlatform implements PlatformService {
|
|
|
134
158
|
const tn = schtasksTaskName(taskId);
|
|
135
159
|
const tr = schtasksTr("run", taskId);
|
|
136
160
|
|
|
137
|
-
//
|
|
138
|
-
|
|
161
|
+
// Build trigger XML elements
|
|
162
|
+
const triggerElements: string[] = [];
|
|
163
|
+
if (task.frontmatter.triggers_enabled) {
|
|
164
|
+
for (const trigger of task.frontmatter.triggers ?? []) {
|
|
165
|
+
try {
|
|
166
|
+
triggerElements.push(triggerToXml(trigger));
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error(`Invalid trigger: ${err}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Always include a dummy trigger so startTask (/run) works
|
|
173
|
+
if (triggerElements.length === 0) {
|
|
174
|
+
triggerElements.push(`<TimeTrigger><StartBoundary>2000-01-01T00:00:00</StartBoundary></TimeTrigger>`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Write XML and register via schtasks — gives us full control over
|
|
178
|
+
// settings like MultipleInstancesPolicy that schtasks flags don't expose.
|
|
179
|
+
const xml = buildTaskXml(tr, triggerElements);
|
|
180
|
+
const xmlPath = path.join(CONFIG_DIR, `task-${taskId}.xml`);
|
|
139
181
|
try {
|
|
182
|
+
// schtasks /xml requires UTF-16LE with BOM
|
|
183
|
+
const bom = Buffer.from([0xFF, 0xFE]);
|
|
184
|
+
fs.writeFileSync(xmlPath, Buffer.concat([bom, Buffer.from(xml, "utf16le")]));
|
|
140
185
|
execFileSync("schtasks", [
|
|
141
|
-
"/create", "/tn", tn,
|
|
142
|
-
"/tr", tr,
|
|
143
|
-
"/sc", "ONCE", "/sd", "01/01/2000", "/st", "00:00",
|
|
144
|
-
"/f",
|
|
186
|
+
"/create", "/tn", tn, "/xml", xmlPath, "/f",
|
|
145
187
|
], { encoding: "utf-8", windowsHide: true });
|
|
146
188
|
} catch (err: unknown) {
|
|
147
189
|
const e = err as { stderr?: string };
|
|
148
190
|
console.error(`Failed to create scheduled task ${tn}: ${e.stderr || err}`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Overlay with real schedule triggers if enabled
|
|
152
|
-
if (!task.frontmatter.triggers_enabled) return;
|
|
153
|
-
const triggers = task.frontmatter.triggers || [];
|
|
154
|
-
for (const trigger of triggers) {
|
|
155
|
-
if (trigger.type === "cron") {
|
|
156
|
-
const schedArgs = cronToSchtasksArgs(trigger.value);
|
|
157
|
-
try {
|
|
158
|
-
execFileSync("schtasks", [
|
|
159
|
-
"/create", "/tn", tn,
|
|
160
|
-
"/tr", tr,
|
|
161
|
-
...schedArgs,
|
|
162
|
-
"/f",
|
|
163
|
-
], { encoding: "utf-8", windowsHide: true });
|
|
164
|
-
} catch (err: unknown) {
|
|
165
|
-
const e = err as { stderr?: string };
|
|
166
|
-
console.error(`Failed to create scheduled task ${tn}: ${e.stderr || err}`);
|
|
167
|
-
}
|
|
168
|
-
} else if (trigger.type === "once") {
|
|
169
|
-
// "once" triggers use ISO datetime: "2026-03-28T09:00"
|
|
170
|
-
const [datePart, timePart] = trigger.value.split("T");
|
|
171
|
-
if (!datePart || !timePart) {
|
|
172
|
-
console.error(`Invalid once trigger value: ${trigger.value}`);
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
// schtasks expects MM/DD/YYYY date format
|
|
176
|
-
const [year, month, day] = datePart.split("-");
|
|
177
|
-
const sd = `${month}/${day}/${year}`;
|
|
178
|
-
const st = timePart.slice(0, 5);
|
|
179
|
-
try {
|
|
180
|
-
execFileSync("schtasks", [
|
|
181
|
-
"/create", "/tn", tn,
|
|
182
|
-
"/tr", tr,
|
|
183
|
-
"/sc", "ONCE", "/sd", sd, "/st", st,
|
|
184
|
-
"/f",
|
|
185
|
-
], { encoding: "utf-8", windowsHide: true });
|
|
186
|
-
} catch (err: unknown) {
|
|
187
|
-
const e = err as { stderr?: string };
|
|
188
|
-
console.error(`Failed to create once task ${tn}: ${e.stderr || err}`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
+
} finally {
|
|
192
|
+
try { fs.unlinkSync(xmlPath); } catch { /* ignore */ }
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
195
|
|
package/src/update-checker.ts
CHANGED
|
@@ -5,8 +5,12 @@ import { spawnCommand } from "./spawn-command.js";
|
|
|
5
5
|
import { getPlatform } from "./platform/index.js";
|
|
6
6
|
|
|
7
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const packageRoot = path.join(__dirname, "..");
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8")) as { version: string };
|
|
10
|
+
|
|
11
|
+
/** True when running from a source checkout (has .git) rather than a global npm install. */
|
|
12
|
+
export const isDevBuild = fs.existsSync(path.join(packageRoot, ".git"));
|
|
13
|
+
export const currentVersion = isDevBuild ? `${pkg.version}-dev` : pkg.version;
|
|
10
14
|
|
|
11
15
|
let latestVersion: string | null = null;
|
|
12
16
|
let lastCheckTime = 0;
|
|
@@ -16,6 +20,7 @@ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
|
16
20
|
* Check the npm registry for the latest version of palmier.
|
|
17
21
|
*/
|
|
18
22
|
export async function checkForUpdate(): Promise<void> {
|
|
23
|
+
if (isDevBuild) return;
|
|
19
24
|
const now = Date.now();
|
|
20
25
|
if (now - lastCheckTime < CHECK_INTERVAL_MS) return;
|
|
21
26
|
lastCheckTime = now;
|
package/dist/pairing.d.ts
DELETED
package/dist/pairing.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
|
|
2
|
-
const CODE_LENGTH = 6;
|
|
3
|
-
export const PAIRING_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
|
|
4
|
-
export function generatePairingCode() {
|
|
5
|
-
const bytes = new Uint8Array(CODE_LENGTH);
|
|
6
|
-
crypto.getRandomValues(bytes);
|
|
7
|
-
return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
|
|
8
|
-
}
|
|
9
|
-
//# sourceMappingURL=pairing.js.map
|
package/src/pairing.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
|
|
2
|
-
const CODE_LENGTH = 6;
|
|
3
|
-
|
|
4
|
-
export const PAIRING_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
|
|
5
|
-
|
|
6
|
-
export function generatePairingCode(): string {
|
|
7
|
-
const bytes = new Uint8Array(CODE_LENGTH);
|
|
8
|
-
crypto.getRandomValues(bytes);
|
|
9
|
-
return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
|
|
10
|
-
}
|