palmier 0.2.5 → 0.2.7

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 +286 -219
  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 +12 -9
  8. package/dist/agents/codex.d.ts +1 -1
  9. package/dist/agents/codex.js +12 -10
  10. package/dist/agents/gemini.d.ts +1 -1
  11. package/dist/agents/gemini.js +13 -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 +0 -22
  18. package/dist/commands/init.js +59 -95
  19. package/dist/commands/lan.d.ts +8 -0
  20. package/dist/commands/lan.js +51 -0
  21. package/dist/commands/mcpserver.js +12 -27
  22. package/dist/commands/pair.d.ts +1 -1
  23. package/dist/commands/pair.js +52 -56
  24. package/dist/commands/plan-generation.md +24 -32
  25. package/dist/commands/restart.d.ts +5 -0
  26. package/dist/commands/restart.js +9 -0
  27. package/dist/commands/run.js +311 -124
  28. package/dist/commands/serve.d.ts +1 -1
  29. package/dist/commands/serve.js +77 -17
  30. package/dist/commands/task-cleanup.d.ts +14 -0
  31. package/dist/commands/task-cleanup.js +84 -0
  32. package/dist/config.js +3 -17
  33. package/dist/events.d.ts +9 -0
  34. package/dist/events.js +46 -0
  35. package/dist/index.js +15 -0
  36. package/dist/platform/linux.d.ts +2 -0
  37. package/dist/platform/linux.js +22 -1
  38. package/dist/platform/platform.d.ts +4 -0
  39. package/dist/platform/windows.d.ts +3 -0
  40. package/dist/platform/windows.js +99 -82
  41. package/dist/rpc-handler.d.ts +2 -1
  42. package/dist/rpc-handler.js +43 -52
  43. package/dist/spawn-command.d.ts +29 -6
  44. package/dist/spawn-command.js +38 -15
  45. package/dist/transports/http-transport.d.ts +1 -1
  46. package/dist/transports/http-transport.js +103 -18
  47. package/dist/transports/nats-transport.d.ts +4 -2
  48. package/dist/transports/nats-transport.js +3 -4
  49. package/dist/types.d.ts +5 -5
  50. package/package.json +5 -3
  51. package/src/agents/agent.ts +8 -3
  52. package/src/agents/claude.ts +44 -43
  53. package/src/agents/codex.ts +11 -12
  54. package/src/agents/gemini.ts +12 -10
  55. package/src/agents/openclaw.ts +8 -7
  56. package/src/agents/shared-prompt.ts +10 -8
  57. package/src/commands/agents.ts +11 -0
  58. package/src/commands/info.ts +0 -24
  59. package/src/commands/init.ts +62 -119
  60. package/src/commands/lan.ts +58 -0
  61. package/src/commands/mcpserver.ts +12 -31
  62. package/src/commands/pair.ts +50 -63
  63. package/src/commands/plan-generation.md +24 -32
  64. package/src/commands/restart.ts +9 -0
  65. package/src/commands/run.ts +375 -143
  66. package/src/commands/serve.ts +96 -17
  67. package/src/config.ts +3 -18
  68. package/src/cross-spawn.d.ts +5 -0
  69. package/src/events.ts +51 -0
  70. package/src/index.ts +17 -0
  71. package/src/platform/linux.ts +25 -1
  72. package/src/platform/platform.ts +6 -0
  73. package/src/platform/windows.ts +100 -89
  74. package/src/rpc-handler.ts +46 -55
  75. package/src/spawn-command.ts +120 -83
  76. package/src/transports/http-transport.ts +123 -19
  77. package/src/transports/nats-transport.ts +4 -4
  78. package/src/types.ts +6 -8
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "child_process";
2
- import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
2
+ import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
3
3
  // On Windows we need a shell so .cmd shims resolve correctly.
4
4
  const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
5
5
  export class CodexAgent {
@@ -10,8 +10,8 @@ export class CodexAgent {
10
10
  args: ["exec", "--skip-git-repo-check", prompt],
11
11
  };
12
12
  }
13
- getTaskRunCommandLine(task, prompt, extraPermissions) {
14
- prompt = (prompt ?? (task.body || task.frontmatter.user_prompt)) + TASK_OUTCOME_SUFFIX;
13
+ getTaskRunCommandLine(task, retryPrompt, extraPermissions) {
14
+ const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
15
15
  // TODO: Update sandbox to workspace-write once https://github.com/openai/codex/issues/12572
16
16
  // is fixed.
17
17
  const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
@@ -20,22 +20,24 @@ export class CodexAgent {
20
20
  args.push("--config");
21
21
  args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
22
22
  }
23
- args.push(prompt);
24
- args.push("resume", "--last");
25
- return { command: "codex", args };
23
+ args.push("-"); // read prompt from stdin
24
+ if (retryPrompt) {
25
+ args.push("resume", "--last");
26
+ } // continue mode for retries
27
+ return { command: "codex", args, stdin: prompt };
26
28
  }
27
29
  async init() {
28
30
  try {
29
- execSync("codex --version", { shell: SHELL });
31
+ execSync("codex --version", { stdio: "ignore", shell: SHELL });
30
32
  }
31
33
  catch {
32
34
  return false;
33
35
  }
34
36
  try {
35
- execSync("codex mcp add palmier palmier mcpserver", { shell: SHELL });
37
+ execSync("codex mcp add palmier palmier mcpserver", { stdio: "ignore", shell: SHELL });
36
38
  }
37
- catch (err) {
38
- console.warn("Warning: failed to install MCP for Codex:", err instanceof Error ? err.message : err);
39
+ catch {
40
+ // MCP registration is best-effort; agent still works without it
39
41
  }
40
42
  return true;
41
43
  }
@@ -2,7 +2,7 @@ import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import type { AgentTool, CommandLine } from "./agent.js";
3
3
  export declare class GeminiAgent implements AgentTool {
4
4
  getPlanGenerationCommandLine(prompt: string): CommandLine;
5
- getTaskRunCommandLine(task: ParsedTask, prompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
5
+ getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
6
6
  init(): Promise<boolean>;
7
7
  }
8
8
  //# sourceMappingURL=gemini.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "child_process";
2
- import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
2
+ import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
3
3
  // On Windows we need a shell so .cmd shims resolve correctly.
4
4
  const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
5
5
  export class GeminiAgent {
@@ -10,9 +10,10 @@ export class GeminiAgent {
10
10
  args: ["--approval-mode", "auto_edit", "--prompt", prompt],
11
11
  };
12
12
  }
13
- getTaskRunCommandLine(task, prompt, extraPermissions) {
14
- prompt = prompt ?? (task.body || task.frontmatter.user_prompt);
15
- const args = ["--resume", "--prompt", prompt + TASK_OUTCOME_SUFFIX];
13
+ getTaskRunCommandLine(task, retryPrompt, extraPermissions) {
14
+ const prompt = retryPrompt ?? (task.body || task.frontmatter.user_prompt);
15
+ const fullPrompt = AGENT_INSTRUCTIONS + "\n\n" + prompt;
16
+ const args = ["--prompt", "-"];
16
17
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
17
18
  if (allPerms.length > 0) {
18
19
  args.push("--allowed-tools");
@@ -20,20 +21,23 @@ export class GeminiAgent {
20
21
  args.push(p.name);
21
22
  }
22
23
  }
23
- return { command: "gemini", args };
24
+ if (retryPrompt) {
25
+ args.push("--resume");
26
+ } // continue mode for retries
27
+ return { command: "gemini", args, stdin: fullPrompt };
24
28
  }
25
29
  async init() {
26
30
  try {
27
- execSync("gemini --version", { shell: SHELL });
31
+ execSync("gemini --version", { stdio: "ignore", shell: SHELL });
28
32
  }
29
33
  catch {
30
34
  return false;
31
35
  }
32
36
  try {
33
- execSync("gemini mcp add --scope user palmier palmier mcpserver", { shell: SHELL });
37
+ execSync("gemini mcp add --scope user palmier palmier mcpserver", { stdio: "ignore", shell: SHELL });
34
38
  }
35
- catch (err) {
36
- console.warn("Warning: failed to install MCP for Gemini:", err instanceof Error ? err.message : err);
39
+ catch {
40
+ // MCP registration is best-effort; agent still works without it
37
41
  }
38
42
  return true;
39
43
  }
@@ -1,8 +1,8 @@
1
1
  import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import type { AgentTool, CommandLine } from "./agent.js";
3
- export declare class ClaudeAgent implements AgentTool {
3
+ export declare class OpenClawAgent implements AgentTool {
4
4
  getPlanGenerationCommandLine(prompt: string): CommandLine;
5
- getTaskRunCommandLine(task: ParsedTask, prompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
5
+ getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
6
6
  init(): Promise<boolean>;
7
7
  }
8
8
  //# sourceMappingURL=openclaw.d.ts.map
@@ -1,20 +1,21 @@
1
1
  import { execSync } from "child_process";
2
- import { TASK_OUTCOME_SUFFIX } from "./shared-prompt.js";
3
- export class ClaudeAgent {
2
+ import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
3
+ export class OpenClawAgent {
4
4
  getPlanGenerationCommandLine(prompt) {
5
5
  return {
6
6
  command: "openclaw",
7
- args: ["agent", "--message", prompt],
7
+ args: ["agent", "--local", "--agent", "main", "--message", prompt],
8
8
  };
9
9
  }
10
- getTaskRunCommandLine(task, prompt, extraPermissions) {
11
- prompt = (prompt ?? (task.body || task.frontmatter.user_prompt)) + TASK_OUTCOME_SUFFIX;
12
- const args = ["agent", "--message", prompt];
10
+ getTaskRunCommandLine(task, retryPrompt, extraPermissions) {
11
+ const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
12
+ // OpenClaw does not support stdin as prompt.
13
+ const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
13
14
  return { command: "openclaw", args };
14
15
  }
15
16
  async init() {
16
17
  try {
17
- execSync("openclaw --version");
18
+ execSync("openclaw --version", { stdio: "ignore" });
18
19
  }
19
20
  catch {
20
21
  return false;
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Appended to the end of every task prompt.
3
- * Instructs the agent to output a clear success/failure marker
4
- * so that palmier can determine the task outcome.
2
+ * Instructions prepended or injected as system prompt for every task invocation.
3
+ * Instructs the agent to output structured markers so palmier can determine
4
+ * the task outcome, report files, and permission/input requests.
5
5
  */
6
- export declare const TASK_OUTCOME_SUFFIX = "\n\n---\nIf you generate report or output files, print each file name on its own line prefixed with [PALMIER_REPORT]: e.g.\n[PALMIER_REPORT] report.md\n[PALMIER_REPORT] summary.md\n\nWhen you are done, output exactly one of these markers as the very last line:\n- Success: [PALMIER_TASK_SUCCESS]\n- Failure: [PALMIER_TASK_FAILURE]\nDo not wrap them in code blocks or add text on the same line.\n\nIf the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line prefixed with [PALMIER_PERMISSION]: e.g.\n[PALMIER_PERMISSION] Read | Read file contents from the repository\n[PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm\n[PALMIER_PERMISSION] Write | Write generated output files";
6
+ export declare const AGENT_INSTRUCTIONS = "If you generate report or output files, print each file name on its own line prefixed with [PALMIER_REPORT]: e.g.\n[PALMIER_REPORT] report.md\n[PALMIER_REPORT] summary.md\n\nWhen you are done, output exactly one of these markers as the very last line:\n- Success: [PALMIER_TASK_SUCCESS]\n- Failure: [PALMIER_TASK_FAILURE]\nDo not wrap them in code blocks or add text on the same line.\n\nIf the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line prefixed with [PALMIER_PERMISSION]: e.g.\n[PALMIER_PERMISSION] Read | Read file contents from the repository\n[PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm\n[PALMIER_PERMISSION] Write | Write generated output files\n\nIf the task requires information from the user that you do not have (such as credentials, connection strings, API keys, or configuration values), print each required input on its own line prefixed with [PALMIER_INPUT]: e.g.\n[PALMIER_INPUT] What is the database connection string?\n[PALMIER_INPUT] What is the API key for the external service?";
7
7
  export declare const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
8
8
  export declare const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
9
9
  export declare const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
10
10
  export declare const TASK_PERMISSION_PREFIX = "[PALMIER_PERMISSION]";
11
+ export declare const TASK_INPUT_PREFIX = "[PALMIER_INPUT]";
11
12
  //# sourceMappingURL=shared-prompt.d.ts.map
@@ -1,12 +1,9 @@
1
1
  /**
2
- * Appended to the end of every task prompt.
3
- * Instructs the agent to output a clear success/failure marker
4
- * so that palmier can determine the task outcome.
2
+ * Instructions prepended or injected as system prompt for every task invocation.
3
+ * Instructs the agent to output structured markers so palmier can determine
4
+ * the task outcome, report files, and permission/input requests.
5
5
  */
6
- export const TASK_OUTCOME_SUFFIX = `
7
-
8
- ---
9
- If you generate report or output files, print each file name on its own line prefixed with [PALMIER_REPORT]: e.g.
6
+ export const AGENT_INSTRUCTIONS = `If you generate report or output files, print each file name on its own line prefixed with [PALMIER_REPORT]: e.g.
10
7
  [PALMIER_REPORT] report.md
11
8
  [PALMIER_REPORT] summary.md
12
9
 
@@ -18,9 +15,14 @@ Do not wrap them in code blocks or add text on the same line.
18
15
  If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line prefixed with [PALMIER_PERMISSION]: e.g.
19
16
  [PALMIER_PERMISSION] Read | Read file contents from the repository
20
17
  [PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
21
- [PALMIER_PERMISSION] Write | Write generated output files`;
18
+ [PALMIER_PERMISSION] Write | Write generated output files
19
+
20
+ If the task requires information from the user that you do not have (such as credentials, connection strings, API keys, or configuration values), print each required input on its own line prefixed with [PALMIER_INPUT]: e.g.
21
+ [PALMIER_INPUT] What is the database connection string?
22
+ [PALMIER_INPUT] What is the API key for the external service?`;
22
23
  export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
23
24
  export const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
24
25
  export const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
25
26
  export const TASK_PERMISSION_PREFIX = "[PALMIER_PERMISSION]";
27
+ export const TASK_INPUT_PREFIX = "[PALMIER_INPUT]";
26
28
  //# sourceMappingURL=shared-prompt.js.map
@@ -1,7 +1,9 @@
1
1
  import { loadConfig, saveConfig } from "../config.js";
2
2
  import { detectAgents } from "../agents/agent.js";
3
+ import { getPlatform } from "../platform/index.js";
3
4
  export async function agentsCommand() {
4
5
  const config = loadConfig();
6
+ const oldKeys = (config.agents ?? []).map((a) => a.key).sort().join(",");
5
7
  console.log("Detecting installed agents...");
6
8
  const agents = await detectAgents();
7
9
  config.agents = agents;
@@ -15,5 +17,14 @@ export async function agentsCommand() {
15
17
  console.log(` ${a.key} — ${a.label}`);
16
18
  }
17
19
  }
20
+ // Restart daemon if agent list changed so the UI picks it up immediately
21
+ const newKeys = agents.map((a) => a.key).sort().join(",");
22
+ if (newKeys !== oldKeys) {
23
+ try {
24
+ console.log("Agent list changed, restarting daemon...");
25
+ await getPlatform().restartDaemon();
26
+ }
27
+ catch { /* daemon may not be running yet */ }
28
+ }
18
29
  }
19
30
  //# sourceMappingURL=agents.js.map
@@ -1,35 +1,13 @@
1
- import * as os from "os";
2
1
  import { loadConfig } from "../config.js";
3
2
  import { loadSessions } from "../session-store.js";
4
- /**
5
- * Detect the first non-internal IPv4 address.
6
- */
7
- function detectLanIp() {
8
- const interfaces = os.networkInterfaces();
9
- for (const name of Object.keys(interfaces)) {
10
- for (const iface of interfaces[name] ?? []) {
11
- if (iface.family === "IPv4" && !iface.internal) {
12
- return iface.address;
13
- }
14
- }
15
- }
16
- return "127.0.0.1";
17
- }
18
3
  /**
19
4
  * Print host connection info for setting up clients.
20
5
  */
21
6
  export async function infoCommand() {
22
7
  const config = loadConfig();
23
- const mode = config.mode ?? "nats";
24
8
  const sessions = loadSessions();
25
9
  console.log(`Host ID: ${config.hostId}`);
26
- console.log(`Mode: ${mode}`);
27
10
  console.log(`Project root: ${config.projectRoot}`);
28
- if (mode === "lan" || mode === "auto") {
29
- const lanIp = detectLanIp();
30
- const port = config.directPort ?? 7400;
31
- console.log(`LAN address: ${lanIp}:${port}`);
32
- }
33
11
  // Detected agents
34
12
  if (config.agents && config.agents.length > 0) {
35
13
  console.log(`Agents: ${config.agents.map((a) => a.label).join(", ")}`);
@@ -1,10 +1,13 @@
1
1
  import * as readline from "readline";
2
- import { randomUUID, randomBytes } from "crypto";
3
- import { saveConfig } from "../config.js";
2
+ import { loadConfig, saveConfig } from "../config.js";
4
3
  import { detectAgents } from "../agents/agent.js";
5
- import { detectLanIp } from "../transports/http-transport.js";
6
4
  import { getPlatform } from "../platform/index.js";
7
5
  import { pairCommand } from "./pair.js";
6
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
7
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
8
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
9
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
10
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
8
11
  /**
9
12
  * Interactive wizard to provision this host.
10
13
  */
@@ -12,45 +15,58 @@ export async function initCommand() {
12
15
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
13
16
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
14
17
  try {
15
- console.log("\n=== Palmier Host Setup ===\n");
16
- let step = 1;
17
- // Step 1: Access mode
18
- const accessMode = await promptAccessMode(ask, step++);
19
- let registerResponse;
20
- if (accessMode === "full") {
21
- // Step 2: Server URL
22
- const serverUrl = await promptServerUrl(ask, step++);
23
- // Register with server
24
- console.log(`\nRegistering host with ${serverUrl}...`);
25
- registerResponse = await registerHost(serverUrl);
26
- console.log("Host registered successfully.");
18
+ console.log(`\n${bold("=== Palmier Host Setup ===")}\n`);
19
+ console.log(`By continuing, you agree to the ${cyan("Terms of Service")} (https://www.palmier.me/terms)`);
20
+ console.log(`and ${cyan("Privacy Policy")} (https://www.palmier.me/privacy).\n`);
21
+ // Detect agents first abort if none found
22
+ console.log("Detecting installed agents...");
23
+ const agents = await detectAgents();
24
+ if (agents.length === 0) {
25
+ console.log(`\n${red("No agent CLIs detected.")} Palmier requires at least one supported agent CLI.\n`);
26
+ console.log(`See supported agents: https://www.palmier.me/agents\n`);
27
+ console.log(`Install at least one agent CLI, then run ${cyan("palmier init")} again.`);
28
+ rl.close();
29
+ process.exit(1);
27
30
  }
28
- let enableLan = accessMode === "lan";
29
- if (accessMode === "full") {
30
- // Step N: Optional LAN
31
- enableLan = await promptEnableLan(ask, step++);
31
+ console.log(` Found: ${green(agents.map((a) => a.label).join(", "))}\n`);
32
+ // Register with server
33
+ let existingHostId;
34
+ try {
35
+ existingHostId = loadConfig().hostId;
36
+ }
37
+ catch { /* first init */ }
38
+ const serverUrl = "https://app.palmier.me";
39
+ let registerResponse;
40
+ while (true) {
41
+ console.log(`\nRegistering host...`);
42
+ try {
43
+ registerResponse = await registerHost(serverUrl, existingHostId);
44
+ console.log(green("Host registered successfully."));
45
+ break;
46
+ }
47
+ catch (err) {
48
+ console.error(`\n ${red(err instanceof Error ? err.message : String(err))}`);
49
+ const retry = await ask("\nRetry? (Y/n): ");
50
+ if (retry.trim().toLowerCase() === "n") {
51
+ console.log("\nSetup cancelled.");
52
+ rl.close();
53
+ return;
54
+ }
55
+ }
32
56
  }
33
- // Determine mode
34
- const mode = accessMode === "lan" ? "lan" : enableLan ? "auto" : "nats";
35
57
  // Build config
36
58
  const config = {
37
- hostId: registerResponse?.hostId ?? randomUUID(),
59
+ hostId: registerResponse.hostId,
38
60
  projectRoot: process.cwd(),
39
- mode,
61
+ nats: true,
62
+ natsUrl: registerResponse.natsUrl,
63
+ natsWsUrl: registerResponse.natsWsUrl,
64
+ natsToken: registerResponse.natsToken,
65
+ agents,
40
66
  };
41
- if (registerResponse) {
42
- config.natsUrl = registerResponse.natsUrl;
43
- config.natsWsUrl = registerResponse.natsWsUrl;
44
- config.natsToken = registerResponse.natsToken;
45
- }
46
- if (enableLan) {
47
- setupLan(config, step++);
48
- }
49
- console.log("\nDetecting installed agents...");
50
- config.agents = await detectAgents();
51
67
  saveConfig(config);
52
- console.log(`\nHost provisioned (${mode} mode). ID: ${config.hostId}`);
53
- console.log("Config saved to ~/.config/palmier/host.json");
68
+ console.log(`\n${green("Host provisioned")} ID: ${cyan(config.hostId)}`);
69
+ console.log(`Config saved to ${dim("~/.config/palmier/host.json")}`);
54
70
  getPlatform().installDaemon(config);
55
71
  console.log("\nStarting pairing...");
56
72
  rl.close();
@@ -61,77 +77,25 @@ export async function initCommand() {
61
77
  throw err;
62
78
  }
63
79
  }
64
- async function promptAccessMode(ask, step) {
65
- console.log(`Step ${step}: Choose access mode\n`);
66
- console.log(" 1) Full access — built-in email and push notification support");
67
- console.log(" 2) LAN only — no data relayed by palmier server, requires same network");
68
- console.log("");
69
- const answer = await ask("Select [1]: ");
70
- const trimmed = answer.trim();
71
- if (trimmed === "2") {
72
- return "lan";
73
- }
74
- return "full";
75
- }
76
- async function promptServerUrl(ask, step) {
77
- const defaultUrl = "https://www.palmier.me";
78
- console.log(`\nStep ${step}: Enter your Palmier server URL\n`);
79
- while (true) {
80
- const answer = await ask(`Server URL [${defaultUrl}]: `);
81
- const trimmed = (answer.trim() || defaultUrl).replace(/\/+$/, "");
82
- if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
83
- return trimmed;
84
- }
85
- console.log(" URL must start with http:// or https://. Please try again.\n");
86
- }
87
- }
88
- async function registerHost(serverUrl) {
80
+ async function registerHost(serverUrl, existingHostId) {
89
81
  try {
90
82
  const res = await fetch(`${serverUrl}/api/hosts/register`, {
91
83
  method: "POST",
92
84
  headers: { "Content-Type": "application/json" },
93
- body: JSON.stringify({}),
85
+ body: JSON.stringify(existingHostId ? { hostId: existingHostId } : {}),
94
86
  });
95
87
  if (!res.ok) {
96
88
  const body = await res.text();
97
- console.error(`Failed to register host: ${res.status} ${res.statusText}\n${body}`);
98
- process.exit(1);
89
+ throw new Error(`${res.status} ${res.statusText}\n${body}`);
99
90
  }
100
91
  return (await res.json());
101
92
  }
102
93
  catch (err) {
103
- console.error(`Failed to reach server: ${err}`);
104
- process.exit(1);
105
- }
106
- }
107
- async function promptEnableLan(ask, step) {
108
- console.log(`\nStep ${step}: Enable LAN access?\n`);
109
- console.log(" When enabled, the app will automatically use a direct LAN connection");
110
- console.log(" when on the same network, and fall back to the server otherwise.\n");
111
- const answer = await ask("Enable LAN access? (Y/n): ");
112
- return answer.trim().toLowerCase() !== "n";
113
- }
114
- function setupLan(config, step) {
115
- const directPort = 7400;
116
- const directToken = randomBytes(32).toString("hex");
117
- const lanIp = detectLanIp();
118
- config.directPort = directPort;
119
- config.directToken = directToken;
120
- console.log(`\nStep ${step}: LAN setup\n`);
121
- console.log(` LAN IP: ${lanIp}`);
122
- console.log(` Port: ${directPort}`);
123
- console.log("");
124
- const platform = process.platform;
125
- if (platform === "win32") {
126
- console.log(" Firewall: You may need to allow incoming connections on this port:");
127
- console.log(` netsh advfirewall firewall add rule name="Palmier" dir=in action=allow protocol=TCP localport=${directPort}`);
128
- }
129
- else if (platform === "darwin") {
130
- console.log(" Firewall: macOS will prompt you to allow incoming connections automatically.");
131
- }
132
- else {
133
- console.log(" Firewall: You may need to allow incoming connections on this port:");
134
- console.log(` sudo ufw allow ${directPort}/tcp`);
94
+ const message = err instanceof Error ? err.message : String(err);
95
+ if (message.includes("fetch failed") || message.includes("ECONNREFUSED") || message.includes("ENOTFOUND") || message.includes("NetworkError")) {
96
+ throw new Error(`Could not reach ${serverUrl} — check the URL and your network connection.`);
97
+ }
98
+ throw new Error(`Failed to register host: ${message}`);
135
99
  }
136
100
  }
137
101
  //# sourceMappingURL=init.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Start an on-demand LAN server for direct HTTP connections.
3
+ * Generates a pairing code and displays it — no separate `palmier pair` needed.
4
+ */
5
+ export declare function lanCommand(opts: {
6
+ port: number;
7
+ }): Promise<void>;
8
+ //# sourceMappingURL=lan.d.ts.map
@@ -0,0 +1,51 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { loadConfig, CONFIG_DIR } from "../config.js";
4
+ import { createRpcHandler } from "../rpc-handler.js";
5
+ import { startHttpTransport, detectLanIp } from "../transports/http-transport.js";
6
+ const LAN_LOCKFILE = path.join(CONFIG_DIR, "lan.json");
7
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
8
+ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
9
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
10
+ const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
11
+ const CODE_LENGTH = 6;
12
+ function generateCode() {
13
+ const bytes = new Uint8Array(CODE_LENGTH);
14
+ crypto.getRandomValues(bytes);
15
+ return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
16
+ }
17
+ function writeLockfile(port) {
18
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
19
+ fs.writeFileSync(LAN_LOCKFILE, JSON.stringify({ port, pid: process.pid }), "utf-8");
20
+ }
21
+ function removeLockfile() {
22
+ try {
23
+ fs.unlinkSync(LAN_LOCKFILE);
24
+ }
25
+ catch { /* ignore */ }
26
+ }
27
+ /**
28
+ * Start an on-demand LAN server for direct HTTP connections.
29
+ * Generates a pairing code and displays it — no separate `palmier pair` needed.
30
+ */
31
+ export async function lanCommand(opts) {
32
+ const config = loadConfig();
33
+ const port = opts.port;
34
+ const ip = detectLanIp();
35
+ const code = generateCode();
36
+ const handleRpc = createRpcHandler(config);
37
+ // Write lockfile so other palmier processes can discover us
38
+ writeLockfile(port);
39
+ // Clean up on exit
40
+ process.on("SIGINT", () => { removeLockfile(); process.exit(0); });
41
+ process.on("SIGTERM", () => { removeLockfile(); process.exit(0); });
42
+ process.on("exit", removeLockfile);
43
+ // Start the HTTP transport with the pre-generated pairing code
44
+ await startHttpTransport(config, handleRpc, port, code, () => {
45
+ console.log(`\n${bold("Palmier LAN Server")}\n`);
46
+ console.log(` ${cyan("Open the app at:")} ${bold(`http://${ip}:${port}`)}\n`);
47
+ console.log(` ${cyan("Pairing code:")} ${bold(code)}\n`);
48
+ console.log(` ${dim("Press Ctrl+C to stop.")}\n`);
49
+ });
50
+ }
51
+ //# sourceMappingURL=lan.js.map
@@ -6,59 +6,44 @@ import { loadConfig } from "../config.js";
6
6
  import { connectNats } from "../nats-client.js";
7
7
  export async function mcpserverCommand() {
8
8
  const config = loadConfig();
9
- const mode = config.mode ?? "nats";
10
- const useNats = mode === "nats" || mode === "auto";
11
- let nc;
12
- if (useNats) {
13
- nc = await connectNats(config);
14
- }
9
+ const nc = await connectNats(config);
15
10
  const sc = StringCodec();
16
11
  const server = new McpServer({ name: "palmier", version: "1.0.0" }, { capabilities: { tools: {} } });
17
- // send-email requires NATS — only register in nats/auto mode
12
+ // send-push-notification requires NATS — only register when server mode is enabled
18
13
  if (nc) {
19
- server.registerTool("send-email", {
20
- description: "Send an email via the Palmier platform's SMTP relay",
14
+ server.registerTool("send-push-notification", {
15
+ description: "Send a push notification to all paired devices via the Palmier platform",
21
16
  inputSchema: {
22
- to: z.string().describe("Recipient email address(es), comma-separated"),
23
- subject: z.string().describe("Email subject line"),
24
- text: z.string().describe("Plain text body"),
25
- html: z.string().optional().describe("HTML body"),
26
- reply_to: z.string().optional().describe("Reply-To address"),
27
- cc: z.string().optional().describe("CC recipients, comma-separated"),
28
- bcc: z.string().optional().describe("BCC recipients, comma-separated"),
17
+ title: z.string().describe("Notification title"),
18
+ body: z.string().describe("Notification body text"),
29
19
  },
30
20
  }, async (args) => {
31
21
  const payload = {
32
22
  hostId: config.hostId,
33
- to: args.to,
34
- subject: args.subject,
35
- text: args.text,
36
- html: args.html,
37
- replyTo: args.reply_to,
38
- cc: args.cc,
39
- bcc: args.bcc,
23
+ title: args.title,
24
+ body: args.body,
40
25
  };
41
26
  try {
42
- const subject = `host.${config.hostId}.email.send`;
27
+ const subject = `host.${config.hostId}.push.send`;
43
28
  const reply = await nc.request(subject, sc.encode(JSON.stringify(payload)), {
44
29
  timeout: 15_000,
45
30
  });
46
31
  const result = JSON.parse(sc.decode(reply.data));
47
32
  if (result.ok) {
48
33
  return {
49
- content: [{ type: "text", text: `Email sent successfully (messageId: ${result.messageId})` }],
34
+ content: [{ type: "text", text: "Push notification sent successfully" }],
50
35
  };
51
36
  }
52
37
  else {
53
38
  return {
54
- content: [{ type: "text", text: `Failed to send email: ${result.error}` }],
39
+ content: [{ type: "text", text: `Failed to send push notification: ${result.error}` }],
55
40
  isError: true,
56
41
  };
57
42
  }
58
43
  }
59
44
  catch (err) {
60
45
  return {
61
- content: [{ type: "text", text: `Error sending email: ${err}` }],
46
+ content: [{ type: "text", text: `Error sending push notification: ${err}` }],
62
47
  isError: true,
63
48
  };
64
49
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Generate an OTP code and wait for a PWA client to pair.
3
- * Listens on NATS (nats/auto modes) and/or HTTP (lan/auto modes).
3
+ * Listens on NATS always, and also on the LAN server if `palmier lan` is running.
4
4
  */
5
5
  export declare function pairCommand(): Promise<void>;
6
6
  //# sourceMappingURL=pair.d.ts.map