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
|
@@ -1,6 +1,79 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import { validateSession, addSession } from "../session-store.js";
|
|
4
|
+
const PWA_ORIGIN = "https://app.palmier.me";
|
|
5
|
+
const CONTENT_TYPES = {
|
|
6
|
+
".html": "text/html; charset=utf-8",
|
|
7
|
+
".js": "application/javascript",
|
|
8
|
+
".css": "text/css",
|
|
9
|
+
".json": "application/json",
|
|
10
|
+
".png": "image/png",
|
|
11
|
+
".ico": "image/x-icon",
|
|
12
|
+
".woff2": "font/woff2",
|
|
13
|
+
".woff": "font/woff",
|
|
14
|
+
".svg": "image/svg+xml",
|
|
15
|
+
};
|
|
16
|
+
function guessContentType(urlPath) {
|
|
17
|
+
const ext = urlPath.match(/\.[^.]+$/)?.[0] ?? "";
|
|
18
|
+
return CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
19
|
+
}
|
|
20
|
+
async function fetchBuffer(url) {
|
|
21
|
+
const res = await fetch(url);
|
|
22
|
+
if (!res.ok)
|
|
23
|
+
throw new Error(`${res.status} ${res.statusText} for ${url}`);
|
|
24
|
+
return Buffer.from(await res.arrayBuffer());
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Download the PWA from palmier.me into memory.
|
|
28
|
+
* Parses index.html for asset references, then fetches each one.
|
|
29
|
+
*/
|
|
30
|
+
async function downloadPwaAssets() {
|
|
31
|
+
const assets = new Map();
|
|
32
|
+
// 1. Fetch index.html
|
|
33
|
+
const html = await fetchBuffer(`${PWA_ORIGIN}/`);
|
|
34
|
+
assets.set("/", { data: html, contentType: "text/html; charset=utf-8" });
|
|
35
|
+
const htmlStr = html.toString("utf-8");
|
|
36
|
+
// 2. Extract references from HTML (src="..." and href="...")
|
|
37
|
+
// Skip service worker and manifest — they require HTTPS which LAN mode doesn't use
|
|
38
|
+
const SKIP = new Set(["/registerSW.js", "/service-worker.js", "/manifest.webmanifest"]);
|
|
39
|
+
const refRegex = /(?:src|href)="([^"]+)"/g;
|
|
40
|
+
const htmlRefs = new Set();
|
|
41
|
+
let match;
|
|
42
|
+
while ((match = refRegex.exec(htmlStr)) !== null) {
|
|
43
|
+
const ref = match[1];
|
|
44
|
+
if (ref.startsWith("/") && !ref.startsWith("//") && !SKIP.has(ref)) {
|
|
45
|
+
htmlRefs.add(ref);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// 3. Fetch all HTML-referenced assets
|
|
49
|
+
for (const ref of htmlRefs) {
|
|
50
|
+
try {
|
|
51
|
+
const data = await fetchBuffer(`${PWA_ORIGIN}${ref}`);
|
|
52
|
+
assets.set(ref, { data, contentType: guessContentType(ref) });
|
|
53
|
+
// 4. Parse CSS for font url() references
|
|
54
|
+
if (ref.endsWith(".css")) {
|
|
55
|
+
const cssStr = data.toString("utf-8");
|
|
56
|
+
const urlRegex = /url\(["']?([^"')]+)["']?\)/g;
|
|
57
|
+
let cssMatch;
|
|
58
|
+
while ((cssMatch = urlRegex.exec(cssStr)) !== null) {
|
|
59
|
+
let fontRef = cssMatch[1];
|
|
60
|
+
if (fontRef.startsWith("data:"))
|
|
61
|
+
continue;
|
|
62
|
+
// Resolve relative URLs against the CSS file's directory
|
|
63
|
+
if (!fontRef.startsWith("/")) {
|
|
64
|
+
const cssDir = ref.substring(0, ref.lastIndexOf("/") + 1);
|
|
65
|
+
fontRef = cssDir + fontRef;
|
|
66
|
+
}
|
|
67
|
+
htmlRefs.add(fontRef);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.warn(`[pwa] Failed to fetch ${ref}: ${err}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return assets;
|
|
76
|
+
}
|
|
4
77
|
const pendingPairs = new Map();
|
|
5
78
|
export function detectLanIp() {
|
|
6
79
|
const interfaces = os.networkInterfaces();
|
|
@@ -16,20 +89,27 @@ export function detectLanIp() {
|
|
|
16
89
|
/**
|
|
17
90
|
* Start the HTTP transport: Express-like server with RPC, SSE, and health endpoints.
|
|
18
91
|
*/
|
|
19
|
-
export async function startHttpTransport(config, handleRpc) {
|
|
20
|
-
|
|
92
|
+
export async function startHttpTransport(config, handleRpc, port, pairingCode, onReady) {
|
|
93
|
+
// Download PWA assets into memory before starting the server
|
|
94
|
+
console.log("[http] Downloading PWA assets...");
|
|
95
|
+
const pwaAssets = await downloadPwaAssets();
|
|
96
|
+
console.log(`[http] Cached ${pwaAssets.size} PWA assets in memory.`);
|
|
21
97
|
const sseClients = new Set();
|
|
98
|
+
// If a pairing code is provided (from `palmier lan`), pre-register it
|
|
99
|
+
if (pairingCode) {
|
|
100
|
+
const EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours — stays valid while lan server runs
|
|
101
|
+
const timer = setTimeout(() => { pendingPairs.delete(pairingCode); }, EXPIRY_MS);
|
|
102
|
+
pendingPairs.set(pairingCode, {
|
|
103
|
+
resolve: () => { },
|
|
104
|
+
timer,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
22
107
|
function broadcastSseEvent(data) {
|
|
23
108
|
const payload = `data: ${JSON.stringify(data)}\n\n`;
|
|
24
109
|
for (const client of sseClients) {
|
|
25
110
|
client.write(payload);
|
|
26
111
|
}
|
|
27
112
|
}
|
|
28
|
-
function setCorsHeaders(res) {
|
|
29
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
30
|
-
res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
|
|
31
|
-
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
|
32
|
-
}
|
|
33
113
|
function checkAuth(req) {
|
|
34
114
|
const auth = req.headers.authorization;
|
|
35
115
|
if (!auth || !auth.startsWith("Bearer "))
|
|
@@ -60,13 +140,6 @@ export async function startHttpTransport(config, handleRpc) {
|
|
|
60
140
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
61
141
|
}
|
|
62
142
|
const server = http.createServer(async (req, res) => {
|
|
63
|
-
setCorsHeaders(res);
|
|
64
|
-
// Handle CORS preflight
|
|
65
|
-
if (req.method === "OPTIONS") {
|
|
66
|
-
res.writeHead(204);
|
|
67
|
-
res.end();
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
143
|
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
71
144
|
const pathname = url.pathname;
|
|
72
145
|
// Internal event endpoint — localhost only, no auth
|
|
@@ -146,7 +219,6 @@ export async function startHttpTransport(config, handleRpc) {
|
|
|
146
219
|
hostId: config.hostId,
|
|
147
220
|
sessionToken: session.token,
|
|
148
221
|
directUrl: `http://${ip}:${port}`,
|
|
149
|
-
directToken: config.directToken,
|
|
150
222
|
};
|
|
151
223
|
// Resolve the long-poll and clean up
|
|
152
224
|
clearTimeout(pending.timer);
|
|
@@ -159,7 +231,21 @@ export async function startHttpTransport(config, handleRpc) {
|
|
|
159
231
|
}
|
|
160
232
|
return;
|
|
161
233
|
}
|
|
162
|
-
//
|
|
234
|
+
// Serve cached PWA assets for non-API routes (no auth required)
|
|
235
|
+
const isApiRoute = pathname === "/events" || pathname.startsWith("/rpc/");
|
|
236
|
+
if (!isApiRoute) {
|
|
237
|
+
// SPA fallback: serve index.html for unrecognized paths
|
|
238
|
+
const asset = pwaAssets.get(pathname) ?? (pathname !== "/" ? pwaAssets.get("/") : undefined);
|
|
239
|
+
if (asset) {
|
|
240
|
+
res.writeHead(200, { "Content-Type": asset.contentType });
|
|
241
|
+
res.end(asset.data);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
sendJson(res, 404, { error: "Not found" });
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// API endpoints require auth
|
|
163
249
|
if (!checkAuth(req)) {
|
|
164
250
|
sendJson(res, 401, { error: "Unauthorized" });
|
|
165
251
|
return;
|
|
@@ -170,7 +256,6 @@ export async function startHttpTransport(config, handleRpc) {
|
|
|
170
256
|
"Content-Type": "text/event-stream",
|
|
171
257
|
"Cache-Control": "no-cache",
|
|
172
258
|
Connection: "keep-alive",
|
|
173
|
-
"Access-Control-Allow-Origin": "*",
|
|
174
259
|
});
|
|
175
260
|
res.write(":ok\n\n");
|
|
176
261
|
// Send heartbeat every 5 seconds
|
|
@@ -220,7 +305,7 @@ export async function startHttpTransport(config, handleRpc) {
|
|
|
220
305
|
return new Promise((resolve, reject) => {
|
|
221
306
|
server.listen(port, () => {
|
|
222
307
|
console.log(`[http] Listening on port ${port}`);
|
|
223
|
-
|
|
308
|
+
onReady?.();
|
|
224
309
|
// Graceful shutdown
|
|
225
310
|
const shutdown = () => {
|
|
226
311
|
console.log("[http] Shutting down...");
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { type NatsConnection } from "nats";
|
|
1
2
|
import type { HostConfig, RpcMessage } from "../types.js";
|
|
2
3
|
/**
|
|
3
|
-
* Start the NATS transport
|
|
4
|
+
* Start the NATS transport using an existing connection.
|
|
5
|
+
* Subscribe to RPC subjects and dispatch to handler.
|
|
4
6
|
*/
|
|
5
|
-
export declare function startNatsTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown
|
|
7
|
+
export declare function startNatsTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>, nc: NatsConnection): Promise<void>;
|
|
6
8
|
//# sourceMappingURL=nats-transport.d.ts.map
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { StringCodec } from "nats";
|
|
2
|
-
import { connectNats } from "../nats-client.js";
|
|
3
2
|
/**
|
|
4
|
-
* Start the NATS transport
|
|
3
|
+
* Start the NATS transport using an existing connection.
|
|
4
|
+
* Subscribe to RPC subjects and dispatch to handler.
|
|
5
5
|
*/
|
|
6
|
-
export async function startNatsTransport(config, handleRpc) {
|
|
7
|
-
const nc = await connectNats(config);
|
|
6
|
+
export async function startNatsTransport(config, handleRpc, nc) {
|
|
8
7
|
const sc = StringCodec();
|
|
9
8
|
const subject = `host.${config.hostId}.rpc.>`;
|
|
10
9
|
console.log(`[nats] Subscribing to: ${subject}`);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
export interface HostConfig {
|
|
2
2
|
hostId: string;
|
|
3
3
|
projectRoot: string;
|
|
4
|
-
|
|
4
|
+
nats?: boolean;
|
|
5
5
|
natsUrl?: string;
|
|
6
6
|
natsWsUrl?: string;
|
|
7
7
|
natsToken?: string;
|
|
8
|
-
directPort?: number;
|
|
9
|
-
directToken?: string;
|
|
10
8
|
agents?: Array<{
|
|
11
9
|
key: string;
|
|
12
10
|
label: string;
|
|
@@ -21,6 +19,7 @@ export interface TaskFrontmatter {
|
|
|
21
19
|
triggers_enabled: boolean;
|
|
22
20
|
requires_confirmation: boolean;
|
|
23
21
|
permissions?: RequiredPermission[];
|
|
22
|
+
command?: string;
|
|
24
23
|
}
|
|
25
24
|
export interface Trigger {
|
|
26
25
|
type: "cron" | "once";
|
|
@@ -30,13 +29,14 @@ export interface ParsedTask {
|
|
|
30
29
|
frontmatter: TaskFrontmatter;
|
|
31
30
|
body: string;
|
|
32
31
|
}
|
|
33
|
-
export type TaskRunningState = "
|
|
32
|
+
export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
|
|
34
33
|
export interface TaskStatus {
|
|
35
34
|
running_state: TaskRunningState;
|
|
36
35
|
time_stamp: number;
|
|
37
36
|
pending_confirmation?: boolean;
|
|
38
37
|
pending_permission?: RequiredPermission[];
|
|
39
|
-
|
|
38
|
+
pending_input?: string[];
|
|
39
|
+
user_input?: string[];
|
|
40
40
|
}
|
|
41
41
|
export interface HistoryEntry {
|
|
42
42
|
task_id: string;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "palmier",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
6
|
"author": "Hongxu Cai",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "dist/index.js",
|
|
@@ -19,16 +19,18 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
21
21
|
"commander": "^13.1.0",
|
|
22
|
+
"cross-spawn": "^7.0.6",
|
|
22
23
|
"dotenv": "^16.4.7",
|
|
23
24
|
"nats": "^2.29.1",
|
|
24
25
|
"yaml": "^2.7.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
28
|
+
"@types/cross-spawn": "^6.0.6",
|
|
27
29
|
"@types/node": "^22.13.0",
|
|
28
30
|
"tsx": "^4.19.0",
|
|
29
31
|
"typescript": "^5.7.0"
|
|
30
32
|
},
|
|
31
33
|
"engines": {
|
|
32
|
-
"node": ">=
|
|
34
|
+
"node": ">=24.0.0"
|
|
33
35
|
}
|
|
34
36
|
}
|
package/src/agents/agent.ts
CHANGED
|
@@ -2,10 +2,13 @@ import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
|
2
2
|
import { ClaudeAgent } from "./claude.js";
|
|
3
3
|
import { GeminiAgent } from "./gemini.js";
|
|
4
4
|
import { CodexAgent } from "./codex.js";
|
|
5
|
+
import { OpenClawAgent } from "./openclaw.js";
|
|
5
6
|
|
|
6
7
|
export interface CommandLine {
|
|
7
8
|
command: string;
|
|
8
9
|
args: string[];
|
|
10
|
+
/** If provided, the string is written to the process's stdin and then the pipe is closed. */
|
|
11
|
+
stdin?: string;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -16,9 +19,10 @@ export interface AgentTool {
|
|
|
16
19
|
/** Return the command and args used to generate a plan from a prompt. */
|
|
17
20
|
getPlanGenerationCommandLine(prompt: string): CommandLine;
|
|
18
21
|
|
|
19
|
-
/** Return the command and args used to run a task. If
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
+
/** Return the command and args used to run a task. If retryPrompt is provided, use it instead of the task's prompt,
|
|
23
|
+
* and treat it as a continuation of the original run (reuse the same session, etc). extraPermissions are transient
|
|
24
|
+
* permissions granted for this run only (not persisted in frontmatter). */
|
|
25
|
+
getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
|
|
22
26
|
|
|
23
27
|
/** Detect whether the agent CLI is available and perform any agent-specific
|
|
24
28
|
* initialization. Returns true if the agent was detected and initialized successfully. */
|
|
@@ -29,6 +33,7 @@ const agentRegistry: Record<string, AgentTool> = {
|
|
|
29
33
|
claude: new ClaudeAgent(),
|
|
30
34
|
gemini: new GeminiAgent(),
|
|
31
35
|
codex: new CodexAgent(),
|
|
36
|
+
openclaw: new OpenClawAgent(),
|
|
32
37
|
};
|
|
33
38
|
|
|
34
39
|
const agentLabels: Record<string, string> = {
|
package/src/agents/claude.ts
CHANGED
|
@@ -1,43 +1,44 @@
|
|
|
1
|
-
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
-
import { execSync } from "child_process";
|
|
3
|
-
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
// execSync's shell option takes a string (shell path), not boolean.
|
|
7
|
-
// On Windows we need a shell so .cmd shims resolve correctly.
|
|
8
|
-
const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
9
|
-
|
|
10
|
-
export class ClaudeAgent implements AgentTool {
|
|
11
|
-
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
12
|
-
return {
|
|
13
|
-
command: "claude",
|
|
14
|
-
args: ["-p", prompt],
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
getTaskRunCommandLine(task: ParsedTask,
|
|
19
|
-
prompt =
|
|
20
|
-
const args = ["
|
|
21
|
-
|
|
22
|
-
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
23
|
-
for (const p of allPerms) {
|
|
24
|
-
args.push("--allowedTools", p.name);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
|
|
5
|
+
|
|
6
|
+
// execSync's shell option takes a string (shell path), not boolean.
|
|
7
|
+
// On Windows we need a shell so .cmd shims resolve correctly.
|
|
8
|
+
const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
9
|
+
|
|
10
|
+
export class ClaudeAgent implements AgentTool {
|
|
11
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
12
|
+
return {
|
|
13
|
+
command: "claude",
|
|
14
|
+
args: ["-p", prompt],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
19
|
+
const prompt = retryPrompt ?? (task.body || task.frontmatter.user_prompt);
|
|
20
|
+
const args = ["--permission-mode", "acceptEdits", "--append-system-prompt", AGENT_INSTRUCTIONS, "-p"];
|
|
21
|
+
|
|
22
|
+
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
23
|
+
for (const p of allPerms) {
|
|
24
|
+
args.push("--allowedTools", p.name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (retryPrompt) {args.push("-c");} // continue mode for retries
|
|
28
|
+
return { command: "claude", args, stdin: prompt };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async init(): Promise<boolean> {
|
|
32
|
+
try {
|
|
33
|
+
execSync("claude --version", { stdio: "ignore", shell: SHELL });
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
execSync("claude mcp add --transport stdio palmier --scope user -- palmier mcpserver", { stdio: "ignore", shell: SHELL });
|
|
39
|
+
} catch {
|
|
40
|
+
// MCP registration is best-effort; agent still works without it
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/agents/codex.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
3
|
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
-
import {
|
|
4
|
+
import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
|
|
5
5
|
|
|
6
6
|
// On Windows we need a shell so .cmd shims resolve correctly.
|
|
7
7
|
const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
@@ -11,12 +11,12 @@ export class CodexAgent implements AgentTool {
|
|
|
11
11
|
// TODO: fill in
|
|
12
12
|
return {
|
|
13
13
|
command: "codex",
|
|
14
|
-
args: ["exec", "--skip-git-repo-check",prompt],
|
|
14
|
+
args: ["exec", "--skip-git-repo-check", prompt],
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
getTaskRunCommandLine(task: ParsedTask,
|
|
19
|
-
prompt = (
|
|
18
|
+
getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
19
|
+
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
20
20
|
// TODO: Update sandbox to workspace-write once https://github.com/openai/codex/issues/12572
|
|
21
21
|
// is fixed.
|
|
22
22
|
const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
|
|
@@ -26,23 +26,22 @@ export class CodexAgent implements AgentTool {
|
|
|
26
26
|
args.push("--config");
|
|
27
27
|
args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
|
|
28
28
|
}
|
|
29
|
+
args.push("-"); // read prompt from stdin
|
|
29
30
|
|
|
30
|
-
args.push(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return { command: "codex", args };
|
|
31
|
+
if (retryPrompt) {args.push("resume", "--last");} // continue mode for retries
|
|
32
|
+
return { command: "codex", args, stdin: prompt };
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
async init(): Promise<boolean> {
|
|
37
36
|
try {
|
|
38
|
-
execSync("codex --version", { shell: SHELL });
|
|
37
|
+
execSync("codex --version", { stdio: "ignore", shell: SHELL });
|
|
39
38
|
} catch {
|
|
40
39
|
return false;
|
|
41
40
|
}
|
|
42
41
|
try {
|
|
43
|
-
execSync("codex mcp add palmier palmier mcpserver", { shell: SHELL });
|
|
44
|
-
} catch
|
|
45
|
-
|
|
42
|
+
execSync("codex mcp add palmier palmier mcpserver", { stdio: "ignore", shell: SHELL });
|
|
43
|
+
} catch {
|
|
44
|
+
// MCP registration is best-effort; agent still works without it
|
|
46
45
|
}
|
|
47
46
|
return true;
|
|
48
47
|
}
|
package/src/agents/gemini.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
3
|
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
-
import {
|
|
4
|
+
import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
|
|
5
5
|
|
|
6
6
|
// On Windows we need a shell so .cmd shims resolve correctly.
|
|
7
7
|
const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
@@ -11,13 +11,14 @@ export class GeminiAgent implements AgentTool {
|
|
|
11
11
|
// TODO: fill in
|
|
12
12
|
return {
|
|
13
13
|
command: "gemini",
|
|
14
|
-
args: ["--approval-mode", "auto_edit","--prompt", prompt],
|
|
14
|
+
args: ["--approval-mode", "auto_edit", "--prompt", prompt],
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
getTaskRunCommandLine(task: ParsedTask,
|
|
19
|
-
prompt =
|
|
20
|
-
const
|
|
18
|
+
getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
19
|
+
const prompt = retryPrompt ?? (task.body || task.frontmatter.user_prompt);
|
|
20
|
+
const fullPrompt = AGENT_INSTRUCTIONS + "\n\n" + prompt;
|
|
21
|
+
const args = ["--prompt", "-"];
|
|
21
22
|
|
|
22
23
|
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
23
24
|
if (allPerms.length > 0) {
|
|
@@ -27,19 +28,20 @@ export class GeminiAgent implements AgentTool {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
if (retryPrompt) {args.push("--resume");} // continue mode for retries
|
|
32
|
+
return { command: "gemini", args, stdin: fullPrompt };
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
async init(): Promise<boolean> {
|
|
34
36
|
try {
|
|
35
|
-
execSync("gemini --version", { shell: SHELL });
|
|
37
|
+
execSync("gemini --version", { stdio: "ignore", shell: SHELL });
|
|
36
38
|
} catch {
|
|
37
39
|
return false;
|
|
38
40
|
}
|
|
39
41
|
try {
|
|
40
|
-
execSync("gemini mcp add --scope user palmier palmier mcpserver", { shell: SHELL });
|
|
41
|
-
} catch
|
|
42
|
-
|
|
42
|
+
execSync("gemini mcp add --scope user palmier palmier mcpserver", { stdio: "ignore", shell: SHELL });
|
|
43
|
+
} catch {
|
|
44
|
+
// MCP registration is best-effort; agent still works without it
|
|
43
45
|
}
|
|
44
46
|
return true;
|
|
45
47
|
}
|
package/src/agents/openclaw.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
3
|
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
-
import {
|
|
4
|
+
import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
|
|
5
5
|
|
|
6
|
-
export class
|
|
6
|
+
export class OpenClawAgent implements AgentTool {
|
|
7
7
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
8
8
|
return {
|
|
9
9
|
command: "openclaw",
|
|
10
|
-
args: ["agent", "--message", prompt],
|
|
10
|
+
args: ["agent", "--local", "--agent", "main", "--message", prompt],
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
getTaskRunCommandLine(task: ParsedTask,
|
|
15
|
-
prompt = (
|
|
16
|
-
|
|
14
|
+
getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
15
|
+
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
16
|
+
// OpenClaw does not support stdin as prompt.
|
|
17
|
+
const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
|
|
17
18
|
|
|
18
19
|
return { command: "openclaw", args };
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
async init(): Promise<boolean> {
|
|
22
23
|
try {
|
|
23
|
-
execSync("openclaw --version");
|
|
24
|
+
execSync("openclaw --version", { stdio: "ignore" });
|
|
24
25
|
} catch {
|
|
25
26
|
return false;
|
|
26
27
|
}
|
|
@@ -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
|
|
|
23
24
|
export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
|
|
24
25
|
export const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
|
|
25
26
|
export const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
|
|
26
27
|
export const TASK_PERMISSION_PREFIX = "[PALMIER_PERMISSION]";
|
|
28
|
+
export const TASK_INPUT_PREFIX = "[PALMIER_INPUT]";
|
package/src/commands/agents.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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
|
|
|
4
5
|
export async function agentsCommand(): Promise<void> {
|
|
5
6
|
const config = loadConfig();
|
|
7
|
+
const oldKeys = (config.agents ?? []).map((a) => a.key).sort().join(",");
|
|
6
8
|
|
|
7
9
|
console.log("Detecting installed agents...");
|
|
8
10
|
const agents = await detectAgents();
|
|
@@ -17,4 +19,13 @@ export async function agentsCommand(): Promise<void> {
|
|
|
17
19
|
console.log(` ${a.key} — ${a.label}`);
|
|
18
20
|
}
|
|
19
21
|
}
|
|
22
|
+
|
|
23
|
+
// Restart daemon if agent list changed so the UI picks it up immediately
|
|
24
|
+
const newKeys = agents.map((a) => a.key).sort().join(",");
|
|
25
|
+
if (newKeys !== oldKeys) {
|
|
26
|
+
try {
|
|
27
|
+
console.log("Agent list changed, restarting daemon...");
|
|
28
|
+
await getPlatform().restartDaemon();
|
|
29
|
+
} catch { /* daemon may not be running yet */ }
|
|
30
|
+
}
|
|
20
31
|
}
|
package/src/commands/info.ts
CHANGED
|
@@ -1,40 +1,16 @@
|
|
|
1
|
-
import * as os from "os";
|
|
2
1
|
import { loadConfig } from "../config.js";
|
|
3
2
|
import { loadSessions } from "../session-store.js";
|
|
4
3
|
|
|
5
|
-
/**
|
|
6
|
-
* Detect the first non-internal IPv4 address.
|
|
7
|
-
*/
|
|
8
|
-
function detectLanIp(): string {
|
|
9
|
-
const interfaces = os.networkInterfaces();
|
|
10
|
-
for (const name of Object.keys(interfaces)) {
|
|
11
|
-
for (const iface of interfaces[name] ?? []) {
|
|
12
|
-
if (iface.family === "IPv4" && !iface.internal) {
|
|
13
|
-
return iface.address;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return "127.0.0.1";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
4
|
/**
|
|
21
5
|
* Print host connection info for setting up clients.
|
|
22
6
|
*/
|
|
23
7
|
export async function infoCommand(): Promise<void> {
|
|
24
8
|
const config = loadConfig();
|
|
25
|
-
const mode = config.mode ?? "nats";
|
|
26
9
|
const sessions = loadSessions();
|
|
27
10
|
|
|
28
11
|
console.log(`Host ID: ${config.hostId}`);
|
|
29
|
-
console.log(`Mode: ${mode}`);
|
|
30
12
|
console.log(`Project root: ${config.projectRoot}`);
|
|
31
13
|
|
|
32
|
-
if (mode === "lan" || mode === "auto") {
|
|
33
|
-
const lanIp = detectLanIp();
|
|
34
|
-
const port = config.directPort ?? 7400;
|
|
35
|
-
console.log(`LAN address: ${lanIp}:${port}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
14
|
// Detected agents
|
|
39
15
|
if (config.agents && config.agents.length > 0) {
|
|
40
16
|
console.log(`Agents: ${config.agents.map((a) => a.label).join(", ")}`);
|