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.
- package/.github/workflows/ci.yml +16 -0
- package/LICENSE +190 -0
- package/README.md +286 -219
- package/dist/agents/agent.d.ts +6 -3
- package/dist/agents/agent.js +2 -0
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +12 -9
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +12 -10
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +13 -9
- package/dist/agents/openclaw.d.ts +2 -2
- package/dist/agents/openclaw.js +8 -7
- package/dist/agents/shared-prompt.d.ts +5 -4
- package/dist/agents/shared-prompt.js +10 -8
- package/dist/commands/agents.js +11 -0
- package/dist/commands/info.js +0 -22
- package/dist/commands/init.js +59 -95
- package/dist/commands/lan.d.ts +8 -0
- package/dist/commands/lan.js +51 -0
- package/dist/commands/mcpserver.js +12 -27
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +52 -56
- package/dist/commands/plan-generation.md +24 -32
- package/dist/commands/restart.d.ts +5 -0
- package/dist/commands/restart.js +9 -0
- package/dist/commands/run.js +311 -124
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +77 -17
- package/dist/commands/task-cleanup.d.ts +14 -0
- package/dist/commands/task-cleanup.js +84 -0
- package/dist/config.js +3 -17
- package/dist/events.d.ts +9 -0
- package/dist/events.js +46 -0
- package/dist/index.js +15 -0
- package/dist/platform/linux.d.ts +2 -0
- package/dist/platform/linux.js +22 -1
- package/dist/platform/platform.d.ts +4 -0
- package/dist/platform/windows.d.ts +3 -0
- package/dist/platform/windows.js +99 -82
- package/dist/rpc-handler.d.ts +2 -1
- package/dist/rpc-handler.js +43 -52
- package/dist/spawn-command.d.ts +29 -6
- package/dist/spawn-command.js +38 -15
- package/dist/transports/http-transport.d.ts +1 -1
- package/dist/transports/http-transport.js +103 -18
- package/dist/transports/nats-transport.d.ts +4 -2
- package/dist/transports/nats-transport.js +3 -4
- package/dist/types.d.ts +5 -5
- package/package.json +5 -3
- package/src/agents/agent.ts +8 -3
- package/src/agents/claude.ts +44 -43
- package/src/agents/codex.ts +11 -12
- package/src/agents/gemini.ts +12 -10
- package/src/agents/openclaw.ts +8 -7
- package/src/agents/shared-prompt.ts +10 -8
- package/src/commands/agents.ts +11 -0
- package/src/commands/info.ts +0 -24
- package/src/commands/init.ts +62 -119
- package/src/commands/lan.ts +58 -0
- package/src/commands/mcpserver.ts +12 -31
- package/src/commands/pair.ts +50 -63
- package/src/commands/plan-generation.md +24 -32
- package/src/commands/restart.ts +9 -0
- package/src/commands/run.ts +375 -143
- package/src/commands/serve.ts +96 -17
- package/src/config.ts +3 -18
- package/src/cross-spawn.d.ts +5 -0
- package/src/events.ts +51 -0
- package/src/index.ts +17 -0
- package/src/platform/linux.ts +25 -1
- package/src/platform/platform.ts +6 -0
- package/src/platform/windows.ts +100 -89
- package/src/rpc-handler.ts +46 -55
- package/src/spawn-command.ts +120 -83
- package/src/transports/http-transport.ts +123 -19
- package/src/transports/nats-transport.ts +4 -4
- package/src/types.ts +6 -8
package/dist/agents/codex.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import {
|
|
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,
|
|
14
|
-
prompt = (
|
|
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(
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
38
|
-
|
|
39
|
+
catch {
|
|
40
|
+
// MCP registration is best-effort; agent still works without it
|
|
39
41
|
}
|
|
40
42
|
return true;
|
|
41
43
|
}
|
package/dist/agents/gemini.d.ts
CHANGED
|
@@ -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,
|
|
5
|
+
getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
|
|
6
6
|
init(): Promise<boolean>;
|
|
7
7
|
}
|
|
8
8
|
//# sourceMappingURL=gemini.d.ts.map
|
package/dist/agents/gemini.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import {
|
|
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,
|
|
14
|
-
prompt =
|
|
15
|
-
const
|
|
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
|
-
|
|
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
|
|
36
|
-
|
|
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
|
|
3
|
+
export declare class OpenClawAgent implements AgentTool {
|
|
4
4
|
getPlanGenerationCommandLine(prompt: string): CommandLine;
|
|
5
|
-
getTaskRunCommandLine(task: ParsedTask,
|
|
5
|
+
getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
|
|
6
6
|
init(): Promise<boolean>;
|
|
7
7
|
}
|
|
8
8
|
//# sourceMappingURL=openclaw.d.ts.map
|
package/dist/agents/openclaw.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
|
-
import {
|
|
3
|
-
export class
|
|
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,
|
|
11
|
-
prompt = (
|
|
12
|
-
|
|
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
|
-
*
|
|
3
|
-
* Instructs the agent to output
|
|
4
|
-
*
|
|
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
|
|
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
|
-
*
|
|
3
|
-
* Instructs the agent to output
|
|
4
|
-
*
|
|
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
|
|
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
|
package/dist/commands/agents.js
CHANGED
|
@@ -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
|
package/dist/commands/info.js
CHANGED
|
@@ -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(", ")}`);
|
package/dist/commands/init.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import * as readline from "readline";
|
|
2
|
-
import {
|
|
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("
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
console.log(
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
59
|
+
hostId: registerResponse.hostId,
|
|
38
60
|
projectRoot: process.cwd(),
|
|
39
|
-
|
|
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(`\
|
|
53
|
-
console.log(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
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
|
|
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-
|
|
12
|
+
// send-push-notification requires NATS — only register when server mode is enabled
|
|
18
13
|
if (nc) {
|
|
19
|
-
server.registerTool("send-
|
|
20
|
-
description: "Send
|
|
14
|
+
server.registerTool("send-push-notification", {
|
|
15
|
+
description: "Send a push notification to all paired devices via the Palmier platform",
|
|
21
16
|
inputSchema: {
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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}.
|
|
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:
|
|
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
|
|
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
|
|
46
|
+
content: [{ type: "text", text: `Error sending push notification: ${err}` }],
|
|
62
47
|
isError: true,
|
|
63
48
|
};
|
|
64
49
|
}
|
package/dist/commands/pair.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generate an OTP code and wait for a PWA client to pair.
|
|
3
|
-
* Listens on NATS
|
|
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
|