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.
- package/.github/workflows/ci.yml +16 -0
- package/LICENSE +190 -0
- package/README.md +288 -223
- 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 +15 -9
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +14 -10
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +15 -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 +17 -10
- package/dist/commands/init.d.ts +2 -10
- package/dist/commands/init.js +171 -163
- package/dist/commands/mcpserver.js +11 -21
- 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 +295 -114
- package/dist/commands/serve.js +83 -4
- package/dist/commands/sessions.js +1 -4
- package/dist/commands/task-cleanup.d.ts +14 -0
- package/dist/commands/task-cleanup.js +84 -0
- package/dist/config.js +1 -1
- package/dist/events.d.ts +10 -0
- package/dist/events.js +29 -0
- package/dist/index.js +14 -7
- package/dist/platform/index.d.ts +4 -0
- package/dist/platform/index.js +12 -0
- package/dist/platform/linux.d.ts +13 -0
- package/dist/platform/linux.js +207 -0
- package/dist/platform/platform.d.ts +24 -0
- package/dist/platform/platform.js +2 -0
- package/dist/platform/windows.d.ts +14 -0
- package/dist/platform/windows.js +218 -0
- package/dist/rpc-handler.d.ts +2 -1
- package/dist/rpc-handler.js +53 -55
- package/dist/spawn-command.d.ts +32 -6
- package/dist/spawn-command.js +38 -13
- package/dist/transports/http-transport.js +2 -7
- package/dist/transports/nats-transport.d.ts +4 -2
- package/dist/transports/nats-transport.js +3 -4
- package/dist/types.d.ts +4 -2
- package/package.json +5 -3
- package/src/agents/agent.ts +8 -3
- package/src/agents/claude.ts +44 -39
- package/src/agents/codex.ts +14 -12
- package/src/agents/gemini.ts +15 -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 +18 -10
- package/src/commands/init.ts +201 -186
- package/src/commands/mcpserver.ts +11 -22
- package/src/commands/plan-generation.md +24 -32
- package/src/commands/restart.ts +9 -0
- package/src/commands/run.ts +367 -133
- package/src/commands/serve.ts +101 -5
- package/src/commands/sessions.ts +1 -4
- package/src/config.ts +1 -1
- package/src/cross-spawn.d.ts +5 -0
- package/src/events.ts +38 -0
- package/src/index.ts +16 -7
- package/src/platform/index.ts +16 -0
- package/src/platform/linux.ts +231 -0
- package/src/platform/platform.ts +31 -0
- package/src/platform/windows.ts +234 -0
- package/src/rpc-handler.ts +56 -63
- package/src/spawn-command.ts +120 -78
- package/src/transports/http-transport.ts +2 -5
- package/src/transports/nats-transport.ts +4 -4
- package/src/types.ts +4 -2
- package/src/systemd.ts +0 -164
package/src/commands/init.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
14
|
-
server?: string;
|
|
15
|
-
lan?: boolean;
|
|
16
|
-
host?: string;
|
|
17
|
-
port?: number;
|
|
18
|
-
}
|
|
10
|
+
type AskFn = (q: string) => Promise<string>;
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
*
|
|
20
|
+
* Interactive wizard to provision this host.
|
|
38
21
|
*/
|
|
39
|
-
async function
|
|
40
|
-
const
|
|
41
|
-
const
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
46
|
+
console.log(` Found: ${green(agents.map((a) => a.label).join(", "))}\n`);
|
|
75
47
|
|
|
76
|
-
|
|
77
|
-
hostId: string;
|
|
78
|
-
natsUrl: string;
|
|
79
|
-
natsWsUrl: string;
|
|
80
|
-
natsToken: string;
|
|
81
|
-
};
|
|
48
|
+
let step = 1;
|
|
82
49
|
|
|
83
|
-
|
|
84
|
-
const
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
65
|
+
// Determine mode
|
|
66
|
+
const mode: HostConfig["mode"] =
|
|
67
|
+
accessMode === "lan" ? "lan" : enableLan ? "auto" : "nats";
|
|
104
68
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
console.log("Starting pairing...");
|
|
140
|
-
await pairCommand();
|
|
141
|
-
}
|
|
156
|
+
config.agents = agents;
|
|
157
|
+
saveConfig(config);
|
|
142
158
|
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
157
|
-
|
|
179
|
+
const answer = await ask("Select [1]: ");
|
|
180
|
+
const trimmed = answer.trim();
|
|
158
181
|
|
|
159
|
-
if (
|
|
160
|
-
return "
|
|
182
|
+
if (trimmed === "2") {
|
|
183
|
+
return "lan";
|
|
161
184
|
}
|
|
162
|
-
return "
|
|
185
|
+
return "full";
|
|
163
186
|
}
|
|
164
187
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
26
|
+
// send-push-notification requires NATS — only register in nats/auto mode
|
|
27
27
|
if (nc) {
|
|
28
28
|
server.registerTool(
|
|
29
|
-
"send-
|
|
29
|
+
"send-push-notification",
|
|
30
30
|
{
|
|
31
|
-
description: "Send
|
|
31
|
+
description: "Send a push notification to all paired devices via the Palmier platform",
|
|
32
32
|
inputSchema: {
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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}.
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
###
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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:**
|