palmier 0.2.6 → 0.2.8

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 (46) hide show
  1. package/.github/workflows/publish.yml +24 -0
  2. package/CLAUDE.md +9 -9
  3. package/README.md +288 -288
  4. package/dist/agents/shared-prompt.js +16 -16
  5. package/dist/commands/info.js +0 -22
  6. package/dist/commands/init.js +27 -123
  7. package/dist/commands/lan.d.ts +8 -0
  8. package/dist/commands/lan.js +51 -0
  9. package/dist/commands/mcpserver.js +2 -7
  10. package/dist/commands/pair.d.ts +1 -1
  11. package/dist/commands/pair.js +52 -56
  12. package/dist/commands/run.js +36 -41
  13. package/dist/commands/serve.d.ts +1 -1
  14. package/dist/commands/serve.js +14 -33
  15. package/dist/config.js +3 -17
  16. package/dist/events.d.ts +3 -4
  17. package/dist/events.js +25 -8
  18. package/dist/index.js +8 -0
  19. package/dist/rpc-handler.js +5 -29
  20. package/dist/transports/http-transport.d.ts +1 -1
  21. package/dist/transports/http-transport.js +103 -18
  22. package/dist/types.d.ts +1 -3
  23. package/package.json +44 -36
  24. package/src/agents/claude.ts +44 -44
  25. package/src/agents/shared-prompt.ts +28 -28
  26. package/src/commands/info.ts +0 -24
  27. package/src/commands/init.ts +29 -150
  28. package/src/commands/lan.ts +58 -0
  29. package/src/commands/mcpserver.ts +2 -10
  30. package/src/commands/pair.ts +50 -63
  31. package/src/commands/run.ts +619 -633
  32. package/src/commands/serve.ts +14 -31
  33. package/src/config.ts +3 -18
  34. package/src/events.ts +23 -10
  35. package/src/index.ts +9 -0
  36. package/src/nats-client.ts +15 -15
  37. package/src/rpc-handler.ts +388 -414
  38. package/src/transports/http-transport.ts +123 -19
  39. package/src/types.ts +62 -66
  40. package/dist/commands/hook.d.ts +0 -7
  41. package/dist/commands/hook.js +0 -208
  42. package/dist/commands/task-cleanup.d.ts +0 -14
  43. package/dist/commands/task-cleanup.js +0 -84
  44. package/dist/commands/task-generation.md +0 -28
  45. package/dist/systemd.d.ts +0 -20
  46. package/dist/systemd.js +0 -145
@@ -1,8 +1,6 @@
1
1
  import * as readline from "readline";
2
- import { randomUUID, randomBytes } from "crypto";
3
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";
8
6
  import type { HostConfig } from "../types.js";
@@ -13,7 +11,6 @@ const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
13
11
  const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
14
12
  const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
15
13
  const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
16
- const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
17
14
  const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
18
15
 
19
16
  /**
@@ -33,11 +30,8 @@ export async function initCommand(): Promise<void> {
33
30
  const agents = await detectAgents();
34
31
 
35
32
  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`);
33
+ console.log(`\n${red("No agent CLIs detected.")} Palmier requires at least one supported agent CLI.\n`);
34
+ console.log(`See supported agents: https://www.palmier.me/agents\n`);
41
35
  console.log(`Install at least one agent CLI, then run ${cyan("palmier init")} again.`);
42
36
  rl.close();
43
37
  process.exit(1);
@@ -45,118 +39,44 @@ export async function initCommand(): Promise<void> {
45
39
 
46
40
  console.log(` Found: ${green(agents.map((a) => a.label).join(", "))}\n`);
47
41
 
48
- let step = 1;
49
-
50
- // Step 1: Access mode
51
- const accessMode = await promptAccessMode(ask, step++);
52
-
53
- let serverUrl: string | undefined;
54
- if (accessMode === "full") {
55
- // Step 2: Server URL
56
- serverUrl = await promptServerUrl(ask, step++);
57
- }
58
-
59
- let enableLan = accessMode === "lan";
60
- if (accessMode === "full") {
61
- // Step N: Optional LAN
62
- enableLan = await promptEnableLan(ask, step++);
63
- }
64
-
65
- // Determine mode
66
- const mode: HostConfig["mode"] =
67
- accessMode === "lan" ? "lan" : enableLan ? "auto" : "nats";
68
-
69
- // Prepare LAN details for summary
70
- let lanIp: string | undefined;
71
- const directPort = 7400;
72
- if (enableLan) {
73
- lanIp = detectLanIp();
74
- }
75
-
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("");
87
-
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
- }
94
-
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
- }
42
+ // Register with server
43
+ let existingHostId: string | undefined;
44
+ try { existingHostId = loadConfig().hostId; } catch { /* first init */ }
45
+
46
+ const serverUrl = "https://app.palmier.me";
47
+ let registerResponse: { hostId: string; natsUrl: string; natsWsUrl: string; natsToken: string };
48
+
49
+ while (true) {
50
+ console.log(`\nRegistering host...`);
51
+ try {
52
+ registerResponse = await registerHost(serverUrl, existingHostId);
53
+ console.log(green("Host registered successfully."));
54
+ break;
55
+ } catch (err) {
56
+ console.error(`\n ${red(err instanceof Error ? err.message : String(err))}`);
57
+ const retry = await ask("\nRetry? (Y/n): ");
58
+ if (retry.trim().toLowerCase() === "n") {
59
+ console.log("\nSetup cancelled.");
60
+ rl.close();
61
+ return;
118
62
  }
119
63
  }
120
64
  }
121
65
 
122
66
  // Build config
123
67
  const config: HostConfig = {
124
- hostId: registerResponse?.hostId ?? randomUUID(),
68
+ hostId: registerResponse.hostId,
125
69
  projectRoot: process.cwd(),
126
- mode,
70
+ nats: true,
71
+ natsUrl: registerResponse.natsUrl,
72
+ natsWsUrl: registerResponse.natsWsUrl,
73
+ natsToken: registerResponse.natsToken,
74
+ agents,
127
75
  };
128
76
 
129
- if (registerResponse) {
130
- config.natsUrl = registerResponse.natsUrl;
131
- config.natsWsUrl = registerResponse.natsWsUrl;
132
- config.natsToken = registerResponse.natsToken;
133
- }
134
-
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
- }
155
-
156
- config.agents = agents;
157
77
  saveConfig(config);
158
78
 
159
- console.log(`\n${green("Host provisioned")} ${dim(`(${mode} mode)`)} ID: ${cyan(config.hostId)}`);
79
+ console.log(`\n${green("Host provisioned")} ID: ${cyan(config.hostId)}`);
160
80
  console.log(`Config saved to ${dim("~/.config/palmier/host.json")}`);
161
81
 
162
82
  getPlatform().installDaemon(config);
@@ -170,36 +90,6 @@ export async function initCommand(): Promise<void> {
170
90
  }
171
91
  }
172
92
 
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")}`);
177
- console.log("");
178
-
179
- const answer = await ask("Select [1]: ");
180
- const trimmed = answer.trim();
181
-
182
- if (trimmed === "2") {
183
- return "lan";
184
- }
185
- return "full";
186
- }
187
-
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;
198
- }
199
-
200
- console.log(" URL must start with http:// or https://. Please try again.\n");
201
- }
202
- }
203
93
 
204
94
  async function registerHost(
205
95
  serverUrl: string,
@@ -231,14 +121,3 @@ async function registerHost(
231
121
  throw new Error(`Failed to register host: ${message}`);
232
122
  }
233
123
  }
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`);
239
-
240
- const answer = await ask("Enable LAN access? (Y/n): ");
241
- return answer.trim().toLowerCase() !== "n";
242
- }
243
-
244
-
@@ -0,0 +1,58 @@
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
+
7
+ const LAN_LOCKFILE = path.join(CONFIG_DIR, "lan.json");
8
+
9
+ const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
10
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
11
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
12
+
13
+ const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
14
+ const CODE_LENGTH = 6;
15
+
16
+ function generateCode(): string {
17
+ const bytes = new Uint8Array(CODE_LENGTH);
18
+ crypto.getRandomValues(bytes);
19
+ return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
20
+ }
21
+
22
+ function writeLockfile(port: number): void {
23
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
24
+ fs.writeFileSync(LAN_LOCKFILE, JSON.stringify({ port, pid: process.pid }), "utf-8");
25
+ }
26
+
27
+ function removeLockfile(): void {
28
+ try { fs.unlinkSync(LAN_LOCKFILE); } catch { /* ignore */ }
29
+ }
30
+
31
+ /**
32
+ * Start an on-demand LAN server for direct HTTP connections.
33
+ * Generates a pairing code and displays it — no separate `palmier pair` needed.
34
+ */
35
+ export async function lanCommand(opts: { port: number }): Promise<void> {
36
+ const config = loadConfig();
37
+ const port = opts.port;
38
+ const ip = detectLanIp();
39
+ const code = generateCode();
40
+
41
+ const handleRpc = createRpcHandler(config);
42
+
43
+ // Write lockfile so other palmier processes can discover us
44
+ writeLockfile(port);
45
+
46
+ // Clean up on exit
47
+ process.on("SIGINT", () => { removeLockfile(); process.exit(0); });
48
+ process.on("SIGTERM", () => { removeLockfile(); process.exit(0); });
49
+ process.on("exit", removeLockfile);
50
+
51
+ // Start the HTTP transport with the pre-generated pairing code
52
+ await startHttpTransport(config, handleRpc, port, code, () => {
53
+ console.log(`\n${bold("Palmier LAN Server")}\n`);
54
+ console.log(` ${cyan("Open the app at:")} ${bold(`http://${ip}:${port}`)}\n`);
55
+ console.log(` ${cyan("Pairing code:")} ${bold(code)}\n`);
56
+ console.log(` ${dim("Press Ctrl+C to stop.")}\n`);
57
+ });
58
+ }
@@ -4,17 +4,9 @@ import { z } from "zod";
4
4
  import { StringCodec } from "nats";
5
5
  import { loadConfig } from "../config.js";
6
6
  import { connectNats } from "../nats-client.js";
7
- import type { NatsConnection } from "nats";
8
-
9
7
  export async function mcpserverCommand(): Promise<void> {
10
8
  const config = loadConfig();
11
- const mode = config.mode ?? "nats";
12
- const useNats = mode === "nats" || mode === "auto";
13
-
14
- let nc: NatsConnection | undefined;
15
- if (useNats) {
16
- nc = await connectNats(config);
17
- }
9
+ const nc = await connectNats(config);
18
10
 
19
11
  const sc = StringCodec();
20
12
 
@@ -23,7 +15,7 @@ export async function mcpserverCommand(): Promise<void> {
23
15
  { capabilities: { tools: {} } }
24
16
  );
25
17
 
26
- // send-push-notification requires NATS — only register in nats/auto mode
18
+ // send-push-notification requires NATS — only register when server mode is enabled
27
19
  if (nc) {
28
20
  server.registerTool(
29
21
  "send-push-notification",
@@ -1,14 +1,16 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
1
3
  import * as http from "node:http";
2
4
  import { StringCodec } from "nats";
3
- import { loadConfig } from "../config.js";
5
+ import { loadConfig, CONFIG_DIR } from "../config.js";
4
6
  import { connectNats } from "../nats-client.js";
5
- import { detectLanIp } from "../transports/http-transport.js";
6
7
  import { addSession } from "../session-store.js";
7
8
  import type { HostConfig } from "../types.js";
8
9
 
9
10
  const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
10
11
  const CODE_LENGTH = 6;
11
12
  const EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
13
+ const LAN_LOCKFILE = path.join(CONFIG_DIR, "lan.json");
12
14
 
13
15
  function generateCode(): string {
14
16
  const bytes = new Uint8Array(CODE_LENGTH);
@@ -18,17 +20,14 @@ function generateCode(): string {
18
20
 
19
21
  function buildPairResponse(config: HostConfig, label?: string) {
20
22
  const session = addSession(label);
21
- const response: Record<string, unknown> = {
23
+ return {
22
24
  hostId: config.hostId,
23
25
  sessionToken: session.token,
24
26
  };
25
-
26
- return response;
27
27
  }
28
28
 
29
29
  /**
30
- * POST to the running serve process and long-poll until paired or expired.
31
- * Returns true if paired, false if expired/failed.
30
+ * POST to the running LAN server and long-poll until paired or expired.
32
31
  */
33
32
  function lanPairRegister(port: number, code: string): Promise<boolean> {
34
33
  const body = JSON.stringify({ code, expiryMs: EXPIRY_MS });
@@ -41,16 +40,14 @@ function lanPairRegister(port: number, code: string): Promise<boolean> {
41
40
  path: "/internal/pair-register",
42
41
  method: "POST",
43
42
  headers: { "Content-Type": "application/json" },
44
- timeout: EXPIRY_MS + 5000, // slightly longer than expiry
43
+ timeout: EXPIRY_MS + 5000,
45
44
  },
46
45
  (res) => {
47
46
  const chunks: Buffer[] = [];
48
47
  res.on("data", (chunk: Buffer) => chunks.push(chunk));
49
48
  res.on("end", () => {
50
49
  try {
51
- const result = JSON.parse(Buffer.concat(chunks).toString("utf-8")) as {
52
- paired: boolean;
53
- };
50
+ const result = JSON.parse(Buffer.concat(chunks).toString("utf-8")) as { paired: boolean };
54
51
  resolve(result.paired);
55
52
  } catch {
56
53
  resolve(false);
@@ -59,29 +56,29 @@ function lanPairRegister(port: number, code: string): Promise<boolean> {
59
56
  },
60
57
  );
61
58
 
62
- req.on("error", (err) => {
63
- console.error(`Failed to reach palmier serve on port ${port}: ${err.message}`);
64
- console.error("Make sure `palmier serve` is running first.");
65
- resolve(false);
66
- });
67
-
68
- req.on("timeout", () => {
69
- req.destroy();
70
- resolve(false);
71
- });
72
-
59
+ req.on("error", () => resolve(false));
60
+ req.on("timeout", () => { req.destroy(); resolve(false); });
73
61
  req.end(body);
74
62
  });
75
63
  }
76
64
 
65
+ /**
66
+ * Read the LAN lockfile to check if `palmier lan` is running.
67
+ */
68
+ function getLanPort(): number | null {
69
+ try {
70
+ const raw = fs.readFileSync(LAN_LOCKFILE, "utf-8");
71
+ return (JSON.parse(raw) as { port: number }).port;
72
+ } catch { return null; }
73
+ }
74
+
77
75
  /**
78
76
  * Generate an OTP code and wait for a PWA client to pair.
79
- * Listens on NATS (nats/auto modes) and/or HTTP (lan/auto modes).
77
+ * Listens on NATS always, and also on the LAN server if `palmier lan` is running.
80
78
  */
81
79
  export async function pairCommand(): Promise<void> {
82
80
  const config = loadConfig();
83
81
  const code = generateCode();
84
- const mode = config.mode ?? "nats";
85
82
 
86
83
  let paired = false;
87
84
 
@@ -98,53 +95,43 @@ export async function pairCommand(): Promise<void> {
98
95
  console.log("");
99
96
  console.log(` ${code}`);
100
97
  console.log("");
101
-
102
- if (mode === "lan" || mode === "auto") {
103
- const ip = detectLanIp();
104
- const port = config.directPort ?? 7400;
105
- console.log(` LAN Address: ${ip}:${port}`);
106
- console.log("");
107
- }
108
-
109
98
  console.log("Code expires in 5 minutes.");
110
99
 
111
- // NATS pairing (nats or auto mode)
112
- if (mode === "nats" || mode === "auto") {
113
- const nc = await connectNats(config);
114
- const sc = StringCodec();
115
- const subject = `pair.${code}`;
116
- const sub = nc.subscribe(subject, { max: 1 });
117
-
118
- cleanups.push(() => {
119
- sub.unsubscribe();
120
- nc.close();
121
- });
100
+ // NATS pairing (always active)
101
+ const nc = await connectNats(config);
102
+ const sc = StringCodec();
103
+ const subject = `pair.${code}`;
104
+ const sub = nc.subscribe(subject, { max: 1 });
122
105
 
123
- (async () => {
124
- for await (const msg of sub) {
125
- if (paired) break;
126
- let label: string | undefined;
127
- try {
128
- if (msg.data && msg.data.length > 0) {
129
- const body = JSON.parse(sc.decode(msg.data)) as { label?: string };
130
- label = body.label;
131
- }
132
- } catch { /* empty body is fine */ }
106
+ cleanups.push(() => {
107
+ sub.unsubscribe();
108
+ nc.close();
109
+ });
133
110
 
134
- const response = buildPairResponse(config, label);
135
- if (msg.reply) {
136
- msg.respond(sc.encode(JSON.stringify(response)));
111
+ (async () => {
112
+ for await (const msg of sub) {
113
+ if (paired) break;
114
+ let label: string | undefined;
115
+ try {
116
+ if (msg.data && msg.data.length > 0) {
117
+ const body = JSON.parse(sc.decode(msg.data)) as { label?: string };
118
+ label = body.label;
137
119
  }
138
- onPaired();
120
+ } catch { /* empty body is fine */ }
121
+
122
+ const response = buildPairResponse(config, label);
123
+ if (msg.reply) {
124
+ msg.respond(sc.encode(JSON.stringify(response)));
139
125
  }
140
- })();
141
- }
126
+ onPaired();
127
+ }
128
+ })();
142
129
 
143
- // LAN pairing — long-poll the running serve process
144
- if (mode === "lan" || mode === "auto") {
145
- const port = config.directPort ?? 7400;
130
+ // LAN pairing — if `palmier lan` is running, also register with it
131
+ const lanPort = getLanPort();
132
+ if (lanPort) {
146
133
  (async () => {
147
- const result = await lanPairRegister(port, code);
134
+ const result = await lanPairRegister(lanPort, code);
148
135
  if (result) onPaired();
149
136
  })();
150
137
  }