palmier 0.3.8 → 0.4.0

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
@@ -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 lets you create, schedule, and run AI agent tasks from your phone or browser, communicating via a cloud relay (NATS) and/or direct HTTP.
9
+ A Node.js CLI that lets you run your own AI agents from your phone. It runs on your machine as a persistent daemon, letting you create, schedule, and monitor agent tasks from any device 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
 
@@ -15,7 +15,7 @@ const agentLabels = {
15
15
  gemini: "Gemini CLI",
16
16
  codex: "Codex CLI",
17
17
  openclaw: "OpenClaw",
18
- copilot: "GitHub Copilot",
18
+ copilot: "Copilot CLI",
19
19
  };
20
20
  export async function detectAgents() {
21
21
  const detected = [];
@@ -5,6 +5,7 @@ import { execSync, exec } from "child_process";
5
5
  import { promisify } from "util";
6
6
  const execAsync = promisify(exec);
7
7
  const UNIT_DIR = path.join(homedir(), ".config", "systemd", "user");
8
+ const PATH_FILE = path.join(homedir(), ".config", "palmier", "user-path");
8
9
  function getTimerName(taskId) {
9
10
  return `palmier-task-${taskId}.timer`;
10
11
  }
@@ -56,6 +57,11 @@ export class LinuxPlatform {
56
57
  installDaemon(config) {
57
58
  fs.mkdirSync(UNIT_DIR, { recursive: true });
58
59
  const palmierBin = process.argv[1] || "palmier";
60
+ // Save the user's shell PATH so restartDaemon can use it later
61
+ // (the daemon itself runs under systemd with a limited PATH).
62
+ const userPath = process.env.PATH || "/usr/local/bin:/usr/bin:/bin";
63
+ fs.mkdirSync(path.dirname(PATH_FILE), { recursive: true });
64
+ fs.writeFileSync(PATH_FILE, userPath, "utf-8");
59
65
  const serviceContent = `[Unit]
60
66
  Description=Palmier Host
61
67
  After=network-online.target
@@ -67,7 +73,7 @@ ExecStart=${palmierBin} serve
67
73
  WorkingDirectory=${config.projectRoot}
68
74
  Restart=on-failure
69
75
  RestartSec=5
70
- Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
76
+ Environment=PATH=${userPath}
71
77
 
72
78
  [Install]
73
79
  WantedBy=default.target
@@ -96,21 +102,22 @@ WantedBy=default.target
96
102
  console.log("\nHost initialization complete!");
97
103
  }
98
104
  async restartDaemon() {
99
- // Update the service file's PATH so the daemon can find agent CLIs.
100
- // Resolve the user's login PATH (not the daemon's limited PATH)
101
- // by sourcing their shell profile.
105
+ // If called from a user's terminal, save the current PATH for future use.
106
+ // If called from the daemon (auto-update), read the saved PATH instead.
107
+ if (process.stdin.isTTY) {
108
+ fs.mkdirSync(path.dirname(PATH_FILE), { recursive: true });
109
+ fs.writeFileSync(PATH_FILE, process.env.PATH || "", "utf-8");
110
+ }
102
111
  const servicePath = path.join(UNIT_DIR, "palmier.service");
103
- if (fs.existsSync(servicePath)) {
104
- let userPath = process.env.PATH || "";
105
- try {
106
- userPath = execSync("bash -lc 'echo $PATH'", { encoding: "utf-8" }).trim();
107
- }
108
- catch { /* fall back to current PATH */ }
109
- const content = fs.readFileSync(servicePath, "utf-8");
110
- const updated = content.replace(/^Environment=PATH=.*/m, `Environment=PATH=${userPath || "/usr/local/bin:/usr/bin:/bin"}`);
111
- if (updated !== content) {
112
- fs.writeFileSync(servicePath, updated, "utf-8");
113
- execSync("systemctl --user daemon-reload", { encoding: "utf-8" });
112
+ if (fs.existsSync(servicePath) && fs.existsSync(PATH_FILE)) {
113
+ const userPath = fs.readFileSync(PATH_FILE, "utf-8").trim();
114
+ if (userPath) {
115
+ const content = fs.readFileSync(servicePath, "utf-8");
116
+ const updated = content.replace(/^Environment=PATH=.*/m, `Environment=PATH=${userPath}`);
117
+ if (updated !== content) {
118
+ fs.writeFileSync(servicePath, updated, "utf-8");
119
+ execSync("systemctl --user daemon-reload", { encoding: "utf-8" });
120
+ }
114
121
  }
115
122
  }
116
123
  execSync("systemctl --user restart palmier.service", { stdio: "inherit" });
@@ -106,9 +106,17 @@ export class WindowsPlatform {
106
106
  console.log("\nHost initialization complete!");
107
107
  }
108
108
  async restartDaemon() {
109
- // Kill the old daemon if we have its PID
110
- if (fs.existsSync(DAEMON_PID_FILE)) {
111
- const oldPid = fs.readFileSync(DAEMON_PID_FILE, "utf-8").trim();
109
+ const script = process.argv[1] || "palmier";
110
+ const oldPid = fs.existsSync(DAEMON_PID_FILE)
111
+ ? fs.readFileSync(DAEMON_PID_FILE, "utf-8").trim()
112
+ : null;
113
+ // Spawn the new daemon before killing the old one.
114
+ this.spawnDaemon(script);
115
+ if (oldPid && oldPid === String(process.pid)) {
116
+ // We ARE the old daemon (auto-update) — exit so only the new one runs.
117
+ process.exit(0);
118
+ }
119
+ else if (oldPid) {
112
120
  try {
113
121
  execFileSync("taskkill", ["/pid", oldPid, "/t", "/f"], { windowsHide: true });
114
122
  }
@@ -116,8 +124,6 @@ export class WindowsPlatform {
116
124
  // Process may have already exited
117
125
  }
118
126
  }
119
- const script = process.argv[1] || "palmier";
120
- this.spawnDaemon(script);
121
127
  }
122
128
  spawnDaemon(script) {
123
129
  const child = nodeSpawn(process.execPath, [script, "serve"], {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.3.8",
3
+ "version": "0.4.0",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -43,7 +43,7 @@ const agentLabels: Record<string, string> = {
43
43
  gemini: "Gemini CLI",
44
44
  codex: "Codex CLI",
45
45
  openclaw: "OpenClaw",
46
- copilot: "GitHub Copilot",
46
+ copilot: "Copilot CLI",
47
47
  };
48
48
 
49
49
  export interface DetectedAgent {
@@ -9,6 +9,7 @@ import type { HostConfig, ParsedTask } from "../types.js";
9
9
  const execAsync = promisify(exec);
10
10
 
11
11
  const UNIT_DIR = path.join(homedir(), ".config", "systemd", "user");
12
+ const PATH_FILE = path.join(homedir(), ".config", "palmier", "user-path");
12
13
 
13
14
  function getTimerName(taskId: string): string {
14
15
  return `palmier-task-${taskId}.timer`;
@@ -70,6 +71,12 @@ export class LinuxPlatform implements PlatformService {
70
71
  fs.mkdirSync(UNIT_DIR, { recursive: true });
71
72
 
72
73
  const palmierBin = process.argv[1] || "palmier";
74
+ // Save the user's shell PATH so restartDaemon can use it later
75
+ // (the daemon itself runs under systemd with a limited PATH).
76
+ const userPath = process.env.PATH || "/usr/local/bin:/usr/bin:/bin";
77
+ fs.mkdirSync(path.dirname(PATH_FILE), { recursive: true });
78
+ fs.writeFileSync(PATH_FILE, userPath, "utf-8");
79
+
73
80
  const serviceContent = `[Unit]
74
81
  Description=Palmier Host
75
82
  After=network-online.target
@@ -81,7 +88,7 @@ ExecStart=${palmierBin} serve
81
88
  WorkingDirectory=${config.projectRoot}
82
89
  Restart=on-failure
83
90
  RestartSec=5
84
- Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
91
+ Environment=PATH=${userPath}
85
92
 
86
93
  [Install]
87
94
  WantedBy=default.target
@@ -113,23 +120,26 @@ WantedBy=default.target
113
120
  }
114
121
 
115
122
  async restartDaemon(): Promise<void> {
116
- // Update the service file's PATH so the daemon can find agent CLIs.
117
- // Resolve the user's login PATH (not the daemon's limited PATH)
118
- // by sourcing their shell profile.
123
+ // If called from a user's terminal, save the current PATH for future use.
124
+ // If called from the daemon (auto-update), read the saved PATH instead.
125
+ if (process.stdin.isTTY) {
126
+ fs.mkdirSync(path.dirname(PATH_FILE), { recursive: true });
127
+ fs.writeFileSync(PATH_FILE, process.env.PATH || "", "utf-8");
128
+ }
129
+
119
130
  const servicePath = path.join(UNIT_DIR, "palmier.service");
120
- if (fs.existsSync(servicePath)) {
121
- let userPath = process.env.PATH || "";
122
- try {
123
- userPath = execSync("bash -lc 'echo $PATH'", { encoding: "utf-8" }).trim();
124
- } catch { /* fall back to current PATH */ }
125
- const content = fs.readFileSync(servicePath, "utf-8");
126
- const updated = content.replace(
127
- /^Environment=PATH=.*/m,
128
- `Environment=PATH=${userPath || "/usr/local/bin:/usr/bin:/bin"}`,
129
- );
130
- if (updated !== content) {
131
- fs.writeFileSync(servicePath, updated, "utf-8");
132
- execSync("systemctl --user daemon-reload", { encoding: "utf-8" });
131
+ if (fs.existsSync(servicePath) && fs.existsSync(PATH_FILE)) {
132
+ const userPath = fs.readFileSync(PATH_FILE, "utf-8").trim();
133
+ if (userPath) {
134
+ const content = fs.readFileSync(servicePath, "utf-8");
135
+ const updated = content.replace(
136
+ /^Environment=PATH=.*/m,
137
+ `Environment=PATH=${userPath}`,
138
+ );
139
+ if (updated !== content) {
140
+ fs.writeFileSync(servicePath, updated, "utf-8");
141
+ execSync("systemctl --user daemon-reload", { encoding: "utf-8" });
142
+ }
133
143
  }
134
144
  }
135
145
  execSync("systemctl --user restart palmier.service", { stdio: "inherit" });
@@ -126,18 +126,24 @@ export class WindowsPlatform implements PlatformService {
126
126
  }
127
127
 
128
128
  async restartDaemon(): Promise<void> {
129
- // Kill the old daemon if we have its PID
130
- if (fs.existsSync(DAEMON_PID_FILE)) {
131
- const oldPid = fs.readFileSync(DAEMON_PID_FILE, "utf-8").trim();
129
+ const script = process.argv[1] || "palmier";
130
+ const oldPid = fs.existsSync(DAEMON_PID_FILE)
131
+ ? fs.readFileSync(DAEMON_PID_FILE, "utf-8").trim()
132
+ : null;
133
+
134
+ // Spawn the new daemon before killing the old one.
135
+ this.spawnDaemon(script);
136
+
137
+ if (oldPid && oldPid === String(process.pid)) {
138
+ // We ARE the old daemon (auto-update) — exit so only the new one runs.
139
+ process.exit(0);
140
+ } else if (oldPid) {
132
141
  try {
133
142
  execFileSync("taskkill", ["/pid", oldPid, "/t", "/f"], { windowsHide: true });
134
143
  } catch {
135
144
  // Process may have already exited
136
145
  }
137
146
  }
138
-
139
- const script = process.argv[1] || "palmier";
140
- this.spawnDaemon(script);
141
147
  }
142
148
 
143
149
  private spawnDaemon(script: string): void {