palmier 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.github/workflows/ci.yml +16 -0
  2. package/LICENSE +190 -0
  3. package/README.md +288 -223
  4. package/dist/agents/agent.d.ts +6 -3
  5. package/dist/agents/agent.js +2 -0
  6. package/dist/agents/claude.d.ts +1 -1
  7. package/dist/agents/claude.js +15 -9
  8. package/dist/agents/codex.d.ts +1 -1
  9. package/dist/agents/codex.js +14 -10
  10. package/dist/agents/gemini.d.ts +1 -1
  11. package/dist/agents/gemini.js +15 -9
  12. package/dist/agents/openclaw.d.ts +2 -2
  13. package/dist/agents/openclaw.js +8 -7
  14. package/dist/agents/shared-prompt.d.ts +5 -4
  15. package/dist/agents/shared-prompt.js +10 -8
  16. package/dist/commands/agents.js +11 -0
  17. package/dist/commands/info.js +17 -10
  18. package/dist/commands/init.d.ts +2 -10
  19. package/dist/commands/init.js +171 -163
  20. package/dist/commands/mcpserver.js +11 -21
  21. package/dist/commands/plan-generation.md +24 -32
  22. package/dist/commands/restart.d.ts +5 -0
  23. package/dist/commands/restart.js +9 -0
  24. package/dist/commands/run.js +295 -114
  25. package/dist/commands/serve.js +83 -4
  26. package/dist/commands/sessions.js +1 -4
  27. package/dist/commands/task-cleanup.d.ts +14 -0
  28. package/dist/commands/task-cleanup.js +84 -0
  29. package/dist/config.js +1 -1
  30. package/dist/events.d.ts +10 -0
  31. package/dist/events.js +29 -0
  32. package/dist/index.js +14 -7
  33. package/dist/platform/index.d.ts +4 -0
  34. package/dist/platform/index.js +12 -0
  35. package/dist/platform/linux.d.ts +13 -0
  36. package/dist/platform/linux.js +207 -0
  37. package/dist/platform/platform.d.ts +24 -0
  38. package/dist/platform/platform.js +2 -0
  39. package/dist/platform/windows.d.ts +14 -0
  40. package/dist/platform/windows.js +218 -0
  41. package/dist/rpc-handler.d.ts +2 -1
  42. package/dist/rpc-handler.js +53 -55
  43. package/dist/spawn-command.d.ts +32 -6
  44. package/dist/spawn-command.js +38 -13
  45. package/dist/transports/http-transport.js +2 -7
  46. package/dist/transports/nats-transport.d.ts +4 -2
  47. package/dist/transports/nats-transport.js +3 -4
  48. package/dist/types.d.ts +4 -2
  49. package/package.json +5 -3
  50. package/src/agents/agent.ts +8 -3
  51. package/src/agents/claude.ts +44 -39
  52. package/src/agents/codex.ts +14 -12
  53. package/src/agents/gemini.ts +15 -10
  54. package/src/agents/openclaw.ts +8 -7
  55. package/src/agents/shared-prompt.ts +10 -8
  56. package/src/commands/agents.ts +11 -0
  57. package/src/commands/info.ts +18 -10
  58. package/src/commands/init.ts +201 -186
  59. package/src/commands/mcpserver.ts +11 -22
  60. package/src/commands/plan-generation.md +24 -32
  61. package/src/commands/restart.ts +9 -0
  62. package/src/commands/run.ts +367 -133
  63. package/src/commands/serve.ts +101 -5
  64. package/src/commands/sessions.ts +1 -4
  65. package/src/config.ts +1 -1
  66. package/src/cross-spawn.d.ts +5 -0
  67. package/src/events.ts +38 -0
  68. package/src/index.ts +16 -7
  69. package/src/platform/index.ts +16 -0
  70. package/src/platform/linux.ts +231 -0
  71. package/src/platform/platform.ts +31 -0
  72. package/src/platform/windows.ts +234 -0
  73. package/src/rpc-handler.ts +56 -63
  74. package/src/spawn-command.ts +120 -78
  75. package/src/transports/http-transport.ts +2 -5
  76. package/src/transports/nats-transport.ts +4 -4
  77. package/src/types.ts +4 -2
  78. package/src/systemd.ts +0 -164
@@ -1,229 +1,244 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import * as os from "os";
4
1
  import * as readline from "readline";
5
2
  import { randomUUID, randomBytes } from "crypto";
6
- import { execSync } from "child_process";
7
- import { homedir } from "os";
8
- import { saveConfig } from "../config.js";
3
+ import { loadConfig, saveConfig } from "../config.js";
9
4
  import { detectAgents } from "../agents/agent.js";
5
+ import { detectLanIp } from "../transports/http-transport.js";
6
+ import { getPlatform } from "../platform/index.js";
10
7
  import { pairCommand } from "./pair.js";
11
8
  import type { HostConfig } from "../types.js";
12
9
 
13
- export interface InitOptions {
14
- server?: string;
15
- lan?: boolean;
16
- host?: string;
17
- port?: number;
18
- }
10
+ type AskFn = (q: string) => Promise<string>;
19
11
 
20
- /**
21
- * Provision this host. Two flows:
22
- * - palmier init --lan → standalone LAN mode, no server needed
23
- * - palmier init --server <url> → register with server, prompts for nats/auto mode
24
- */
25
- export async function initCommand(options: InitOptions): Promise<void> {
26
- if (options.lan) {
27
- await initLan(options);
28
- } else if (options.server) {
29
- await initWithServer(options);
30
- } else {
31
- console.error("Either --lan or --server <url> is required.");
32
- process.exit(1);
33
- }
34
- }
12
+ const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
13
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
14
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
15
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
16
+ const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
17
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
35
18
 
36
19
  /**
37
- * Flow A: Standalone LAN mode. No web server needed.
20
+ * Interactive wizard to provision this host.
38
21
  */
39
- async function initLan(options: InitOptions): Promise<void> {
40
- const hostId = randomUUID();
41
- const directToken = randomBytes(32).toString("hex");
42
- const directPort = options.port ?? 7400;
43
- const lanIp = options.host ?? detectLanIp();
44
-
45
- const config: HostConfig = {
46
- hostId,
47
- mode: "lan",
48
- directPort,
49
- directToken,
50
- projectRoot: process.cwd(),
51
- };
52
-
53
- console.log("Detecting installed agents...");
54
- config.agents = await detectAgents();
55
- saveConfig(config);
56
-
57
- console.log(`Host provisioned (LAN mode). ID: ${hostId}`);
58
- console.log("Config saved to ~/.config/palmier/host.json");
59
-
60
- installSystemdService(config);
61
-
62
- // Auto-enter pair mode so user can connect their PWA immediately
63
- console.log("");
64
- console.log("Starting pairing...");
65
- await pairCommand();
66
- }
22
+ export async function initCommand(): Promise<void> {
23
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
24
+ const ask: AskFn = (q) => new Promise<string>((resolve) => rl.question(q, resolve));
67
25
 
68
- /**
69
- * Flow B: Register directly with server. No provisioning token needed.
70
- */
71
- async function initWithServer(options: InitOptions): Promise<void> {
72
- const serverUrl = options.server!;
26
+ try {
27
+ console.log(`\n${bold("=== Palmier Host Setup ===")}\n`);
28
+ console.log(`By continuing, you agree to the ${cyan("Terms of Service")} (https://www.palmier.me/terms)`);
29
+ console.log(`and ${cyan("Privacy Policy")} (https://www.palmier.me/privacy).\n`);
30
+
31
+ // Detect agents first — abort if none found
32
+ console.log("Detecting installed agents...");
33
+ const agents = await detectAgents();
34
+
35
+ if (agents.length === 0) {
36
+ console.log(`\n${red("No agent CLIs detected.")} Palmier requires at least one of the following:\n`);
37
+ console.log(` - ${bold("Claude Code")} See https://code.claude.com`);
38
+ console.log(` - ${bold("Gemini CLI")} npm install -g @google/gemini-cli`);
39
+ console.log(` - ${bold("Codex CLI")} npm install -g @openai/codex`);
40
+ console.log(` - ${bold("OpenClaw")} See https://github.com/openclaw/openclaw\n`);
41
+ console.log(`Install at least one agent CLI, then run ${cyan("palmier init")} again.`);
42
+ rl.close();
43
+ process.exit(1);
44
+ }
73
45
 
74
- console.log(`Registering host at ${serverUrl}...`);
46
+ console.log(` Found: ${green(agents.map((a) => a.label).join(", "))}\n`);
75
47
 
76
- let registerResponse: {
77
- hostId: string;
78
- natsUrl: string;
79
- natsWsUrl: string;
80
- natsToken: string;
81
- };
48
+ let step = 1;
82
49
 
83
- try {
84
- const res = await fetch(`${serverUrl}/api/hosts/register`, {
85
- method: "POST",
86
- headers: { "Content-Type": "application/json" },
87
- body: JSON.stringify({}),
88
- });
50
+ // Step 1: Access mode
51
+ const accessMode = await promptAccessMode(ask, step++);
89
52
 
90
- if (!res.ok) {
91
- const body = await res.text();
92
- console.error(`Failed to register host: ${res.status} ${res.statusText}\n${body}`);
93
- process.exit(1);
53
+ let serverUrl: string | undefined;
54
+ if (accessMode === "full") {
55
+ // Step 2: Server URL
56
+ serverUrl = await promptServerUrl(ask, step++);
94
57
  }
95
58
 
96
- registerResponse = (await res.json()) as typeof registerResponse;
97
- } catch (err) {
98
- console.error(`Failed to reach server: ${err}`);
99
- process.exit(1);
100
- }
59
+ let enableLan = accessMode === "lan";
60
+ if (accessMode === "full") {
61
+ // Step N: Optional LAN
62
+ enableLan = await promptEnableLan(ask, step++);
63
+ }
101
64
 
102
- // Prompt for connection mode
103
- const mode = await promptMode();
65
+ // Determine mode
66
+ const mode: HostConfig["mode"] =
67
+ accessMode === "lan" ? "lan" : enableLan ? "auto" : "nats";
104
68
 
105
- // Build config
106
- const config: HostConfig = {
107
- hostId: registerResponse.hostId,
108
- projectRoot: process.cwd(),
109
- mode,
110
- natsUrl: registerResponse.natsUrl,
111
- natsWsUrl: registerResponse.natsWsUrl,
112
- natsToken: registerResponse.natsToken,
113
- };
69
+ // Prepare LAN details for summary
70
+ let lanIp: string | undefined;
71
+ const directPort = 7400;
72
+ if (enableLan) {
73
+ lanIp = detectLanIp();
74
+ }
114
75
 
115
- if (mode === "auto") {
116
- const directToken = randomBytes(32).toString("hex");
117
- const directPort = options.port ?? 7400;
118
- const lanIp = options.host ?? detectLanIp();
76
+ // Summary before committing
77
+ console.log(`\n${bold("--- Setup Summary ---")}\n`);
78
+ console.log(` Mode: ${cyan(mode)}${dim(mode === "auto" ? " (NATS + LAN)" : mode === "nats" ? " (NATS only)" : " (LAN only)")}`);
79
+ if (serverUrl) {
80
+ console.log(` Server: ${cyan(serverUrl)}`);
81
+ }
82
+ if (enableLan && lanIp) {
83
+ console.log(` LAN: ${cyan(`${lanIp}:${directPort}`)}`);
84
+ }
85
+ console.log(` Agents: ${green(agents.map((a) => a.label).join(", "))}`);
86
+ console.log("");
119
87
 
120
- config.directPort = directPort;
121
- config.directToken = directToken;
88
+ const confirm = await ask("Proceed? (Y/n): ");
89
+ if (confirm.trim().toLowerCase() === "n") {
90
+ console.log("\nSetup cancelled.");
91
+ rl.close();
92
+ return;
93
+ }
122
94
 
123
- console.log("");
124
- console.log("Direct connection info (for LAN clients):");
125
- console.log(` Address: ${lanIp}:${directPort}`);
126
- console.log(` Token: ${directToken}`);
127
- }
95
+ // Register with server (after confirmation)
96
+ let registerResponse:
97
+ | { hostId: string; natsUrl: string; natsWsUrl: string; natsToken: string }
98
+ | undefined;
99
+
100
+ if (accessMode === "full" && serverUrl) {
101
+ let existingHostId: string | undefined;
102
+ try { existingHostId = loadConfig().hostId; } catch { /* first init */ }
103
+
104
+ while (true) {
105
+ console.log(`\nRegistering host with ${cyan(serverUrl)}...`);
106
+ try {
107
+ registerResponse = await registerHost(serverUrl, existingHostId);
108
+ console.log(green("Host registered successfully."));
109
+ break;
110
+ } catch (err) {
111
+ console.error(`\n ${red(err instanceof Error ? err.message : String(err))}`);
112
+ const retry = await ask("\nRetry? (Y/n): ");
113
+ if (retry.trim().toLowerCase() === "n") {
114
+ console.log("\nSetup cancelled.");
115
+ rl.close();
116
+ return;
117
+ }
118
+ }
119
+ }
120
+ }
128
121
 
129
- console.log("Detecting installed agents...");
130
- config.agents = await detectAgents();
131
- saveConfig(config);
132
- console.log(`Host provisioned (${mode} mode). ID: ${config.hostId}`);
133
- console.log("Config saved to ~/.config/palmier/host.json");
122
+ // Build config
123
+ const config: HostConfig = {
124
+ hostId: registerResponse?.hostId ?? randomUUID(),
125
+ projectRoot: process.cwd(),
126
+ mode,
127
+ };
128
+
129
+ if (registerResponse) {
130
+ config.natsUrl = registerResponse.natsUrl;
131
+ config.natsWsUrl = registerResponse.natsWsUrl;
132
+ config.natsToken = registerResponse.natsToken;
133
+ }
134
134
 
135
- installSystemdService(config);
135
+ if (enableLan) {
136
+ const directToken = randomBytes(32).toString("hex");
137
+ config.directPort = directPort;
138
+ config.directToken = directToken;
139
+
140
+ console.log(`\n LAN IP: ${cyan(lanIp!)}`);
141
+ console.log(` Port: ${cyan(String(directPort))}`);
142
+
143
+ if (process.platform === "win32") {
144
+ console.log(`\n ${yellow("Firewall:")} You may need to allow incoming connections on this port:`);
145
+ console.log(
146
+ dim(` netsh advfirewall firewall add rule name="Palmier" dir=in action=allow protocol=TCP localport=${directPort}`)
147
+ );
148
+ } else if (process.platform === "darwin") {
149
+ console.log(`\n ${yellow("Firewall:")} macOS will prompt you to allow incoming connections automatically.`);
150
+ } else {
151
+ console.log(`\n ${yellow("Firewall:")} You may need to allow incoming connections on this port:`);
152
+ console.log(dim(` sudo ufw allow ${directPort}/tcp`));
153
+ }
154
+ }
136
155
 
137
- // Auto-enter pair mode so user can connect their PWA immediately
138
- console.log("");
139
- console.log("Starting pairing...");
140
- await pairCommand();
141
- }
156
+ config.agents = agents;
157
+ saveConfig(config);
142
158
 
143
- /**
144
- * Prompt user to select connection mode.
145
- */
146
- async function promptMode(): Promise<"nats" | "auto"> {
147
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
148
- const question = (q: string) => new Promise<string>((resolve) => rl.question(q, resolve));
159
+ console.log(`\n${green("Host provisioned")} ${dim(`(${mode} mode)`)} ID: ${cyan(config.hostId)}`);
160
+ console.log(`Config saved to ${dim("~/.config/palmier/host.json")}`);
149
161
 
150
- console.log("");
151
- console.log("Connection mode:");
152
- console.log(" 1) auto — Both LAN and NATS (recommended)");
153
- console.log(" 2) nats — NATS only");
162
+ getPlatform().installDaemon(config);
163
+
164
+ console.log("\nStarting pairing...");
165
+ rl.close();
166
+ await pairCommand();
167
+ } catch (err) {
168
+ rl.close();
169
+ throw err;
170
+ }
171
+ }
172
+
173
+ async function promptAccessMode(ask: AskFn, step: number): Promise<"full" | "lan"> {
174
+ console.log(`${bold(`Step ${step}:`)} Choose access mode\n`);
175
+ console.log(` ${bold("1)")} Full access ${dim("— built-in email and push notification support")}`);
176
+ console.log(` ${bold("2)")} LAN only ${dim("— no data relayed by palmier server, requires same network")}`);
154
177
  console.log("");
155
178
 
156
- const answer = await question("Select [1]: ");
157
- rl.close();
179
+ const answer = await ask("Select [1]: ");
180
+ const trimmed = answer.trim();
158
181
 
159
- if (answer.trim() === "2" || answer.trim().toLowerCase() === "nats") {
160
- return "nats";
182
+ if (trimmed === "2") {
183
+ return "lan";
161
184
  }
162
- return "auto";
185
+ return "full";
163
186
  }
164
187
 
165
- /**
166
- * Detect the first non-internal IPv4 address.
167
- */
168
- function detectLanIp(): string {
169
- const interfaces = os.networkInterfaces();
170
- for (const name of Object.keys(interfaces)) {
171
- for (const iface of interfaces[name] ?? []) {
172
- if (iface.family === "IPv4" && !iface.internal) {
173
- return iface.address;
174
- }
188
+ async function promptServerUrl(ask: AskFn, step: number): Promise<string> {
189
+ const defaultUrl = "https://www.palmier.me";
190
+ console.log(`\n${bold(`Step ${step}:`)} Enter your Palmier server URL\n`);
191
+
192
+ while (true) {
193
+ const answer = await ask(`Server URL [${defaultUrl}]: `);
194
+ const trimmed = (answer.trim() || defaultUrl).replace(/\/+$/, "");
195
+
196
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
197
+ return trimmed;
175
198
  }
199
+
200
+ console.log(" URL must start with http:// or https://. Please try again.\n");
176
201
  }
177
- return "127.0.0.1";
178
202
  }
179
203
 
180
- /**
181
- * Install systemd user service for palmier serve.
182
- */
183
- function installSystemdService(config: HostConfig): void {
184
- const unitDir = path.join(homedir(), ".config", "systemd", "user");
185
- fs.mkdirSync(unitDir, { recursive: true });
186
-
187
- const palmierBin = process.argv[1] || "palmier";
188
-
189
- const serviceContent = `[Unit]
190
- Description=Palmier Host
191
- After=network-online.target
192
- Wants=network-online.target
193
-
194
- [Service]
195
- Type=simple
196
- ExecStart=${palmierBin} serve
197
- WorkingDirectory=${config.projectRoot}
198
- Restart=on-failure
199
- RestartSec=5
200
- Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
201
-
202
- [Install]
203
- WantedBy=default.target
204
- `;
205
-
206
- const servicePath = path.join(unitDir, "palmier.service");
207
- fs.writeFileSync(servicePath, serviceContent, "utf-8");
208
- console.log("Systemd service installed at:", servicePath);
209
-
210
- // Enable and start the service
204
+ async function registerHost(
205
+ serverUrl: string,
206
+ existingHostId?: string,
207
+ ): Promise<{ hostId: string; natsUrl: string; natsWsUrl: string; natsToken: string }> {
211
208
  try {
212
- execSync("systemctl --user daemon-reload", { stdio: "inherit" });
213
- execSync("systemctl --user enable --now palmier.service", { stdio: "inherit" });
214
- console.log("Palmier host service enabled and started.");
215
- } catch (err) {
216
- console.error(`Warning: failed to enable systemd service: ${err}`);
217
- console.error("You may need to start it manually: systemctl --user enable --now palmier.service");
218
- }
209
+ const res = await fetch(`${serverUrl}/api/hosts/register`, {
210
+ method: "POST",
211
+ headers: { "Content-Type": "application/json" },
212
+ body: JSON.stringify(existingHostId ? { hostId: existingHostId } : {}),
213
+ });
219
214
 
220
- // Enable lingering so service runs without active login session
221
- try {
222
- execSync(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
223
- console.log("Login lingering enabled.");
215
+ if (!res.ok) {
216
+ const body = await res.text();
217
+ throw new Error(`${res.status} ${res.statusText}\n${body}`);
218
+ }
219
+
220
+ return (await res.json()) as {
221
+ hostId: string;
222
+ natsUrl: string;
223
+ natsWsUrl: string;
224
+ natsToken: string;
225
+ };
224
226
  } catch (err) {
225
- console.error(`Warning: failed to enable linger: ${err}`);
227
+ const message = err instanceof Error ? err.message : String(err);
228
+ if (message.includes("fetch failed") || message.includes("ECONNREFUSED") || message.includes("ENOTFOUND") || message.includes("NetworkError")) {
229
+ throw new Error(`Could not reach ${serverUrl} — check the URL and your network connection.`);
230
+ }
231
+ throw new Error(`Failed to register host: ${message}`);
226
232
  }
233
+ }
234
+
235
+ async function promptEnableLan(ask: AskFn, step: number): Promise<boolean> {
236
+ console.log(`\n${bold(`Step ${step}:`)} Enable LAN access?\n`);
237
+ console.log(` ${dim("When enabled, the app will automatically use a direct LAN connection")}`);
238
+ console.log(` ${dim("when on the same network, and fall back to the server otherwise.")}\n`);
227
239
 
228
- console.log("\nHost initialization complete!");
240
+ const answer = await ask("Enable LAN access? (Y/n): ");
241
+ return answer.trim().toLowerCase() !== "n";
229
242
  }
243
+
244
+
@@ -23,58 +23,47 @@ export async function mcpserverCommand(): Promise<void> {
23
23
  { capabilities: { tools: {} } }
24
24
  );
25
25
 
26
- // send-email requires NATS — only register in nats/auto mode
26
+ // send-push-notification requires NATS — only register in nats/auto mode
27
27
  if (nc) {
28
28
  server.registerTool(
29
- "send-email",
29
+ "send-push-notification",
30
30
  {
31
- description: "Send an email via the Palmier platform's SMTP relay",
31
+ description: "Send a push notification to all paired devices via the Palmier platform",
32
32
  inputSchema: {
33
- to: z.string().describe("Recipient email address(es), comma-separated"),
34
- subject: z.string().describe("Email subject line"),
35
- text: z.string().describe("Plain text body"),
36
- html: z.string().optional().describe("HTML body"),
37
- reply_to: z.string().optional().describe("Reply-To address"),
38
- cc: z.string().optional().describe("CC recipients, comma-separated"),
39
- bcc: z.string().optional().describe("BCC recipients, comma-separated"),
33
+ title: z.string().describe("Notification title"),
34
+ body: z.string().describe("Notification body text"),
40
35
  },
41
36
  },
42
37
  async (args) => {
43
38
  const payload = {
44
39
  hostId: config.hostId,
45
- to: args.to,
46
- subject: args.subject,
47
- text: args.text,
48
- html: args.html,
49
- replyTo: args.reply_to,
50
- cc: args.cc,
51
- bcc: args.bcc,
40
+ title: args.title,
41
+ body: args.body,
52
42
  };
53
43
 
54
44
  try {
55
- const subject = `host.${config.hostId}.email.send`;
45
+ const subject = `host.${config.hostId}.push.send`;
56
46
  const reply = await nc!.request(subject, sc.encode(JSON.stringify(payload)), {
57
47
  timeout: 15_000,
58
48
  });
59
49
  const result = JSON.parse(sc.decode(reply.data)) as {
60
50
  ok?: boolean;
61
- messageId?: string;
62
51
  error?: string;
63
52
  };
64
53
 
65
54
  if (result.ok) {
66
55
  return {
67
- content: [{ type: "text" as const, text: `Email sent successfully (messageId: ${result.messageId})` }],
56
+ content: [{ type: "text" as const, text: "Push notification sent successfully" }],
68
57
  };
69
58
  } else {
70
59
  return {
71
- content: [{ type: "text" as const, text: `Failed to send email: ${result.error}` }],
60
+ content: [{ type: "text" as const, text: `Failed to send push notification: ${result.error}` }],
72
61
  isError: true,
73
62
  };
74
63
  }
75
64
  } catch (err) {
76
65
  return {
77
- content: [{ type: "text" as const, text: `Error sending email: ${err}` }],
66
+ content: [{ type: "text" as const, text: `Error sending push notification: ${err}` }],
78
67
  isError: true,
79
68
  };
80
69
  }
@@ -1,32 +1,24 @@
1
- You are a task planning assistant. Given a task description, produce a detailed Markdown execution plan that an agent can follow step by step. **Do not execute any part of the plan yourself.**
2
-
3
- Your output must begin with a raw YAML frontmatter block (delimited by `---`), followed by the plan body in Markdown. Do NOT wrap the frontmatter in code fences. The very first line of your output must be `---`.
4
-
5
- **Output format:**
6
-
7
- ---
8
- task_name: <short descriptive name, 3-6 words>
9
- ---
10
-
11
- <plan body in Markdown>
12
-
13
- **Frontmatter fields:**
14
-
15
- - `task_name`: A concise label for the task (e.g., "Clean up temp files", "Update system packages", "Backup database daily").
16
-
17
- **Plan body sections:**
18
-
19
- ### 1. Goal
20
- State what the task accomplishes and the expected end state.
21
-
22
- ### 2. Plan
23
- A numbered sequence of concrete, actionable steps. Use sub-steps for complex actions and include conditional branches where behavior may vary (e.g., "If file exists, do A; otherwise, do B"). Each step should be specific enough for the agent to execute without ambiguity.
24
-
25
- ### 3. Output Format (if applicable)
26
- If the task produces a report, email, or other formatted output, specify the exact structure, sections, tone, and any templates the agent should follow.
27
-
28
- Use Markdown formatting throughout — headings, code blocks, and tables where appropriate.
29
-
30
- **Important:** Any relative times or dates in the task description (e.g., "yesterday", "last week") are relative to when the task is executed, not when this plan is generated. The agent must resolve them at execution time.
31
-
32
- **Task description:**
1
+ You are a task planning assistant. Given a task description, produce a Markdown execution plan for an agent. **Do not execute any part of the plan yourself.**
2
+
3
+ Output a raw YAML frontmatter block (delimited by `---`) followed by the plan body. Do NOT wrap frontmatter in code fences. The first line of output must be `---`.
4
+
5
+ ---
6
+ task_name: <short name, 3-6 words>
7
+ ---
8
+
9
+ **Frontmatter:** `task_name` — concise label (e.g., "Clean up temp files", "Backup database daily").
10
+
11
+ **Plan body:**
12
+
13
+ ### 1. Goal
14
+ What the task accomplishes and the expected end state.
15
+
16
+ ### 2. Plan
17
+ Numbered sequence of concrete, actionable steps. Include conditional branches where behavior may vary. Each step must be unambiguous.
18
+
19
+ ### 3. Output Format (if applicable)
20
+ If the task produces formatted output (report, email, etc.), specify structure, sections, tone, and templates.
21
+
22
+ Relative times in the task description (e.g., "yesterday") are relative to execution time, not plan generation time.
23
+
24
+ **Task description:**
@@ -0,0 +1,9 @@
1
+ import { getPlatform } from "../platform/index.js";
2
+
3
+ /**
4
+ * Restart the palmier serve daemon.
5
+ */
6
+ export async function restartCommand(): Promise<void> {
7
+ const platform = getPlatform();
8
+ await platform.restartDaemon();
9
+ }