palmier 0.4.5 → 0.4.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/README.md +29 -31
- package/dist/agents/agent-instructions.md +4 -11
- package/dist/agents/claude.js +3 -3
- package/dist/agents/codex.js +2 -2
- package/dist/agents/copilot.js +3 -3
- package/dist/agents/gemini.js +3 -3
- package/dist/agents/openclaw.js +2 -2
- package/dist/agents/shared-prompt.d.ts +2 -4
- package/dist/agents/shared-prompt.js +9 -4
- package/dist/commands/init.js +31 -2
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +12 -15
- package/dist/commands/run.js +19 -43
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +9 -2
- package/dist/events.d.ts +2 -2
- package/dist/events.js +15 -16
- package/dist/index.js +0 -25
- package/dist/pending-requests.d.ts +27 -0
- package/dist/pending-requests.js +39 -0
- package/dist/rpc-handler.js +15 -8
- package/dist/transports/http-transport.d.ts +4 -2
- package/dist/transports/http-transport.js +226 -77
- package/dist/types.d.ts +7 -16
- package/package.json +1 -1
- package/src/agents/agent-instructions.md +4 -11
- package/src/agents/claude.ts +3 -3
- package/src/agents/codex.ts +2 -2
- package/src/agents/copilot.ts +3 -3
- package/src/agents/gemini.ts +3 -3
- package/src/agents/openclaw.ts +2 -2
- package/src/agents/shared-prompt.ts +12 -6
- package/src/commands/init.ts +34 -3
- package/src/commands/pair.ts +11 -14
- package/src/commands/run.ts +17 -57
- package/src/commands/serve.ts +11 -2
- package/src/events.ts +14 -15
- package/src/index.ts +0 -26
- package/src/pending-requests.ts +55 -0
- package/src/rpc-handler.ts +15 -9
- package/src/transports/http-transport.ts +235 -135
- package/src/types.ts +10 -16
- package/dist/commands/lan.d.ts +0 -8
- package/dist/commands/lan.js +0 -44
- package/dist/commands/notify.d.ts +0 -9
- package/dist/commands/notify.js +0 -43
- package/dist/commands/request-input.d.ts +0 -10
- package/dist/commands/request-input.js +0 -49
- package/dist/lan-lock.d.ts +0 -7
- package/dist/lan-lock.js +0 -18
- package/dist/user-input.d.ts +0 -15
- package/dist/user-input.js +0 -50
- package/src/commands/lan.ts +0 -48
- package/src/commands/notify.ts +0 -44
- package/src/commands/request-input.ts +0 -51
- package/src/lan-lock.ts +0 -16
- package/src/user-input.ts +0 -67
package/dist/types.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface HostConfig {
|
|
|
8
8
|
key: string;
|
|
9
9
|
label: string;
|
|
10
10
|
}>;
|
|
11
|
+
httpPort?: number;
|
|
12
|
+
lanEnabled?: boolean;
|
|
11
13
|
}
|
|
12
14
|
export interface TaskFrontmatter {
|
|
13
15
|
id: string;
|
|
@@ -29,8 +31,6 @@ export interface ParsedTask {
|
|
|
29
31
|
body: string;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
|
-
* State machine: started → (pending_confirmation | pending_permission | pending_input) → finished | aborted | failed
|
|
33
|
-
*
|
|
34
34
|
* - `started`: task is actively running
|
|
35
35
|
* - `finished`: agent completed successfully
|
|
36
36
|
* - `aborted`: user declined confirmation, permission, or input
|
|
@@ -38,26 +38,15 @@ export interface ParsedTask {
|
|
|
38
38
|
*/
|
|
39
39
|
export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
|
|
40
40
|
/**
|
|
41
|
-
* Persisted to `status.json` in the task directory.
|
|
42
|
-
* and
|
|
43
|
-
*
|
|
44
|
-
* Interactive request flow: the run process sets a `pending_*` field and waits
|
|
45
|
-
* for `user_input` to be populated by an RPC call (task.user_input). Only one
|
|
46
|
-
* `pending_*` field is set at a time.
|
|
41
|
+
* Persisted to `status.json` in the task directory. Used for crash detection
|
|
42
|
+
* (checkStaleTasks) and abort signalling. Interactive request flows (confirmation,
|
|
43
|
+
* permission, input) are handled via held HTTP connections on the serve daemon.
|
|
47
44
|
*/
|
|
48
45
|
export interface TaskStatus {
|
|
49
46
|
running_state: TaskRunningState;
|
|
50
47
|
time_stamp: number;
|
|
51
48
|
/** PID of the palmier run process (used on Windows to kill the process tree). */
|
|
52
49
|
pid?: number;
|
|
53
|
-
/** Set when the task has `requires_confirmation` and is awaiting user approval. */
|
|
54
|
-
pending_confirmation?: boolean;
|
|
55
|
-
/** Set when the agent requests permissions not yet granted. Contains the permissions needed. */
|
|
56
|
-
pending_permission?: RequiredPermission[];
|
|
57
|
-
/** Set when the agent requests user input. Contains descriptions of each requested value. */
|
|
58
|
-
pending_input?: string[];
|
|
59
|
-
/** Written by the RPC handler to deliver the user's response to the waiting run process. */
|
|
60
|
-
user_input?: string[];
|
|
61
50
|
}
|
|
62
51
|
export interface HistoryEntry {
|
|
63
52
|
task_id: string;
|
|
@@ -78,5 +67,7 @@ export interface RpcMessage {
|
|
|
78
67
|
method: string;
|
|
79
68
|
params: Record<string, unknown>;
|
|
80
69
|
sessionToken?: string;
|
|
70
|
+
/** Trusted localhost request — skip session validation. */
|
|
71
|
+
localhost?: boolean;
|
|
81
72
|
}
|
|
82
73
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
|
@@ -20,20 +20,13 @@ If the task fails because a tool was denied or you lack the required permissions
|
|
|
20
20
|
[PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
|
|
21
21
|
[PALMIER_PERMISSION] Write | Write generated output files
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## HTTP Endpoints
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution.
|
|
26
26
|
|
|
27
|
-
**Requesting user input** — If you need any information you do not have (credentials, configuration values, preferences, clarifications, etc.) or the task explicitly asks you to get input from the user, do NOT fail the task. Instead, request
|
|
28
|
-
```
|
|
29
|
-
palmier request-input --description "What is the database connection string?" --description "What is the API key?"
|
|
30
|
-
```
|
|
31
|
-
The command blocks until the user responds and prints each value on its own line. If the user aborts, the command exits with a non-zero status.
|
|
27
|
+
**Requesting user input** — If you need any information you do not have (credentials, configuration values, preferences, clarifications, etc.) or the task explicitly asks you to get input from the user, do NOT fail the task. Instead, GET `/request-input?taskId={{TASK_ID}}&descriptions=question+1&descriptions=question+2`. The request blocks until the user responds. The response is `{"values":["answer1","answer2"]}` on success, or `{"aborted":true}` if the user chooses to abort.
|
|
32
28
|
|
|
33
|
-
**Sending push notifications** —
|
|
34
|
-
```
|
|
35
|
-
palmier notify --title "Task Complete" --body "The deployment finished successfully."
|
|
36
|
-
```
|
|
29
|
+
**Sending push notifications** — GET `/notify?title=...&body=...` to send a push notification to the user's devices.
|
|
37
30
|
|
|
38
31
|
---
|
|
39
32
|
|
package/src/agents/claude.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 { getAgentInstructions } from "./shared-prompt.js";
|
|
5
5
|
import { SHELL } from "../platform/index.js";
|
|
6
6
|
|
|
7
7
|
export class ClaudeAgent implements AgentTool {
|
|
@@ -13,8 +13,8 @@ export class ClaudeAgent implements AgentTool {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
-
const prompt =
|
|
17
|
-
const args = ["--permission-mode", "acceptEdits", "-p"];
|
|
16
|
+
const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
17
|
+
const args = ["--permission-mode", "acceptEdits", "-p", "--allowedTools", "WebFetch"];
|
|
18
18
|
|
|
19
19
|
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
20
20
|
for (const p of allPerms) {
|
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 { getAgentInstructions } from "./shared-prompt.js";
|
|
5
5
|
import { SHELL } from "../platform/index.js";
|
|
6
6
|
|
|
7
7
|
export class CodexAgent implements AgentTool {
|
|
@@ -13,7 +13,7 @@ export class CodexAgent implements AgentTool {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
-
const prompt =
|
|
16
|
+
const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
17
17
|
// Using danger-full-access until workspace-write is fixed: https://github.com/openai/codex/issues/12572
|
|
18
18
|
const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
|
|
19
19
|
|
package/src/agents/copilot.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 { getAgentInstructions } from "./shared-prompt.js";
|
|
5
5
|
import { SHELL } from "../platform/index.js";
|
|
6
6
|
|
|
7
7
|
export class CopilotAgent implements AgentTool {
|
|
@@ -13,8 +13,8 @@ export class CopilotAgent implements AgentTool {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
-
const prompt =
|
|
17
|
-
const args = ["-p", prompt];
|
|
16
|
+
const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
17
|
+
const args = ["-p", prompt, "--allowed-tools", "web_fetch"];
|
|
18
18
|
|
|
19
19
|
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
20
20
|
if (allPerms.length > 0) {
|
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 { getAgentInstructions } from "./shared-prompt.js";
|
|
5
5
|
import { SHELL } from "../platform/index.js";
|
|
6
6
|
|
|
7
7
|
export class GeminiAgent implements AgentTool {
|
|
@@ -14,8 +14,8 @@ export class GeminiAgent implements AgentTool {
|
|
|
14
14
|
|
|
15
15
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
16
|
const prompt = followupPrompt ?? (task.body || task.frontmatter.user_prompt);
|
|
17
|
-
const fullPrompt =
|
|
18
|
-
const args = ["--prompt", "-"];
|
|
17
|
+
const fullPrompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + prompt;
|
|
18
|
+
const args = ["--prompt", "--allowed-tools", "web_fetch", "-"];
|
|
19
19
|
|
|
20
20
|
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
21
21
|
if (allPerms.length > 0) {
|
package/src/agents/openclaw.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 { getAgentInstructions } from "./shared-prompt.js";
|
|
5
5
|
|
|
6
6
|
export class OpenClawAgent implements AgentTool {
|
|
7
7
|
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
@@ -12,7 +12,7 @@ export class OpenClawAgent implements AgentTool {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
15
|
-
const prompt =
|
|
15
|
+
const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
16
16
|
// OpenClaw does not support stdin as prompt.
|
|
17
17
|
const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
|
|
18
18
|
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
+
import { loadConfig } from "../config.js";
|
|
4
5
|
|
|
5
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
* Instructions prepended or injected as system prompt for every task invocation.
|
|
9
|
-
* Instructs the agent to output structured markers so palmier can determine
|
|
10
|
-
* the task outcome, report files, and permission/input requests.
|
|
11
|
-
*/
|
|
12
|
-
export const AGENT_INSTRUCTIONS = fs.readFileSync(
|
|
8
|
+
const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
|
|
13
9
|
path.join(__dirname, "agent-instructions.md"),
|
|
14
10
|
"utf-8",
|
|
15
11
|
);
|
|
16
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Agent instructions with the serve daemon's HTTP port and task ID baked in.
|
|
15
|
+
*/
|
|
16
|
+
export function getAgentInstructions(taskId: string): string {
|
|
17
|
+
const port = loadConfig().httpPort ?? 7400;
|
|
18
|
+
return AGENT_INSTRUCTIONS_TEMPLATE
|
|
19
|
+
.replace(/\{\{PORT\}\}/g, String(port))
|
|
20
|
+
.replace(/\{\{TASK_ID\}\}/g, taskId);
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
|
|
18
24
|
export const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
|
|
19
25
|
export const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
|
package/src/commands/init.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { loadConfig, saveConfig } from "../config.js";
|
|
|
3
3
|
import { detectAgents } from "../agents/agent.js";
|
|
4
4
|
import { getPlatform } from "../platform/index.js";
|
|
5
5
|
import { pairCommand } from "./pair.js";
|
|
6
|
+
import { detectLanIp } from "../transports/http-transport.js";
|
|
6
7
|
import type { HostConfig } from "../types.js";
|
|
7
8
|
|
|
8
9
|
type AskFn = (q: string) => Promise<string>;
|
|
@@ -39,6 +40,36 @@ export async function initCommand(): Promise<void> {
|
|
|
39
40
|
|
|
40
41
|
console.log(` Found: ${green(agents.map((a) => a.label).join(", "))}\n`);
|
|
41
42
|
|
|
43
|
+
// LAN mode
|
|
44
|
+
const lanAnswer = await ask("Enable LAN access (direct HTTP from local network)? (y/N): ");
|
|
45
|
+
const lanEnabled = lanAnswer.trim().toLowerCase() === "y";
|
|
46
|
+
|
|
47
|
+
let httpPort = 7400;
|
|
48
|
+
const portLabel = lanEnabled ? "HTTP port for local and LAN access" : "HTTP port for local access";
|
|
49
|
+
const portAnswer = await ask(`${portLabel} (default ${httpPort}): `);
|
|
50
|
+
const parsed = parseInt(portAnswer.trim(), 10);
|
|
51
|
+
if (parsed > 0 && parsed < 65536) httpPort = parsed;
|
|
52
|
+
|
|
53
|
+
// Display summary and ask for confirmation before making any changes
|
|
54
|
+
console.log(`\n${bold("Setup summary:")}\n`);
|
|
55
|
+
console.log(` ${dim("Task storage:")} ${bold(process.cwd())}`);
|
|
56
|
+
console.log(` All tasks and execution data will be stored here.\n`);
|
|
57
|
+
console.log(` ${dim("Local access:")} ${cyan(`http://localhost:${httpPort}`)}`);
|
|
58
|
+
console.log(` Always available — no internet required.\n`);
|
|
59
|
+
if (lanEnabled) {
|
|
60
|
+
const ip = detectLanIp();
|
|
61
|
+
console.log(` ${dim("LAN access:")} ${cyan(`http://${ip}:${httpPort}`)}`);
|
|
62
|
+
console.log(` Accessible from other devices on your local network. Pairing required.\n`);
|
|
63
|
+
}
|
|
64
|
+
console.log(` ${dim("Agents:")} ${agents.map((a) => a.label).join(", ")}\n`);
|
|
65
|
+
|
|
66
|
+
const confirm = await ask("Proceed? (Y/n): ");
|
|
67
|
+
if (confirm.trim().toLowerCase() === "n") {
|
|
68
|
+
console.log("\nSetup cancelled.");
|
|
69
|
+
rl.close();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
42
73
|
// Register with server
|
|
43
74
|
let existingHostId: string | undefined;
|
|
44
75
|
try { existingHostId = loadConfig().hostId; } catch { /* first init */ }
|
|
@@ -63,7 +94,7 @@ export async function initCommand(): Promise<void> {
|
|
|
63
94
|
}
|
|
64
95
|
}
|
|
65
96
|
|
|
66
|
-
// Build config
|
|
97
|
+
// Build and save config
|
|
67
98
|
const config: HostConfig = {
|
|
68
99
|
hostId: registerResponse.hostId,
|
|
69
100
|
projectRoot: process.cwd(),
|
|
@@ -71,11 +102,11 @@ export async function initCommand(): Promise<void> {
|
|
|
71
102
|
natsWsUrl: registerResponse.natsWsUrl,
|
|
72
103
|
natsToken: registerResponse.natsToken,
|
|
73
104
|
agents,
|
|
105
|
+
httpPort,
|
|
106
|
+
lanEnabled,
|
|
74
107
|
};
|
|
75
108
|
|
|
76
109
|
saveConfig(config);
|
|
77
|
-
|
|
78
|
-
console.log(`\n${green("Host provisioned")} ID: ${cyan(config.hostId)}`);
|
|
79
110
|
console.log(`Config saved to ${dim("~/.config/palmier/host.json")}`);
|
|
80
111
|
|
|
81
112
|
getPlatform().installDaemon(config);
|
package/src/commands/pair.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { StringCodec } from "nats";
|
|
|
3
3
|
import { loadConfig } from "../config.js";
|
|
4
4
|
import { connectNats } from "../nats-client.js";
|
|
5
5
|
import { addSession } from "../session-store.js";
|
|
6
|
-
import { getLanPort } from "../lan-lock.js";
|
|
7
6
|
import type { HostConfig } from "../types.js";
|
|
8
7
|
|
|
9
8
|
const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
|
|
@@ -26,9 +25,9 @@ function buildPairResponse(config: HostConfig, label?: string) {
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
|
-
* POST to the running
|
|
28
|
+
* POST to the running serve daemon and long-poll until paired or expired.
|
|
30
29
|
*/
|
|
31
|
-
function
|
|
30
|
+
function httpPairRegister(port: number, code: string): Promise<boolean> {
|
|
32
31
|
const body = JSON.stringify({ code, expiryMs: PAIRING_EXPIRY_MS });
|
|
33
32
|
|
|
34
33
|
return new Promise((resolve) => {
|
|
@@ -36,7 +35,7 @@ function lanPairRegister(port: number, code: string): Promise<boolean> {
|
|
|
36
35
|
{
|
|
37
36
|
hostname: "127.0.0.1",
|
|
38
37
|
port,
|
|
39
|
-
path: "/
|
|
38
|
+
path: "/pair-register",
|
|
40
39
|
method: "POST",
|
|
41
40
|
headers: { "Content-Type": "application/json" },
|
|
42
41
|
timeout: PAIRING_EXPIRY_MS + 5000,
|
|
@@ -63,11 +62,12 @@ function lanPairRegister(port: number, code: string): Promise<boolean> {
|
|
|
63
62
|
|
|
64
63
|
/**
|
|
65
64
|
* Generate an OTP code and wait for a PWA client to pair.
|
|
66
|
-
* Listens on NATS
|
|
65
|
+
* Listens on NATS (server mode) and HTTP (via serve daemon) in parallel.
|
|
67
66
|
*/
|
|
68
67
|
export async function pairCommand(): Promise<void> {
|
|
69
68
|
const config = loadConfig();
|
|
70
69
|
const code = generatePairingCode();
|
|
70
|
+
const httpPort = config.httpPort ?? 7400;
|
|
71
71
|
|
|
72
72
|
let paired = false;
|
|
73
73
|
|
|
@@ -86,7 +86,7 @@ export async function pairCommand(): Promise<void> {
|
|
|
86
86
|
console.log("");
|
|
87
87
|
console.log("Code expires in 5 minutes.");
|
|
88
88
|
|
|
89
|
-
// NATS pairing (
|
|
89
|
+
// NATS pairing (server mode)
|
|
90
90
|
const nc = await connectNats(config);
|
|
91
91
|
const sc = StringCodec();
|
|
92
92
|
const subject = `pair.${code}`;
|
|
@@ -116,14 +116,11 @@ export async function pairCommand(): Promise<void> {
|
|
|
116
116
|
}
|
|
117
117
|
})();
|
|
118
118
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
(
|
|
123
|
-
|
|
124
|
-
if (result) onPaired();
|
|
125
|
-
})();
|
|
126
|
-
}
|
|
119
|
+
// HTTP pairing — register with serve daemon's /pair-register endpoint
|
|
120
|
+
(async () => {
|
|
121
|
+
const result = await httpPairRegister(httpPort, code);
|
|
122
|
+
if (result) onPaired();
|
|
123
|
+
})();
|
|
127
124
|
|
|
128
125
|
// Wait for pairing or timeout
|
|
129
126
|
const start = Date.now();
|
package/src/commands/run.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { getPlatform } from "../platform/index.js";
|
|
|
10
10
|
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
|
|
11
11
|
import type { AgentTool } from "../agents/agent.js";
|
|
12
12
|
import { publishHostEvent } from "../events.js";
|
|
13
|
-
import { waitForUserInput } from "../user-input.js";
|
|
14
13
|
import type { HostConfig, ParsedTask, TaskRunningState, RequiredPermission } from "../types.js";
|
|
15
14
|
import type { NatsConnection } from "nats";
|
|
16
15
|
|
|
@@ -52,7 +51,7 @@ async function invokeAgentWithContinuation(
|
|
|
52
51
|
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, followupPrompt, ctx.transientPermissions);
|
|
53
52
|
const result = await spawnCommand(command, args, {
|
|
54
53
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
55
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
|
|
54
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7400) },
|
|
56
55
|
echoStdout: true,
|
|
57
56
|
resolveOnFailure: true,
|
|
58
57
|
stdin,
|
|
@@ -72,8 +71,7 @@ async function invokeAgentWithContinuation(
|
|
|
72
71
|
|
|
73
72
|
// Permission handling — agent requested permissions
|
|
74
73
|
if (requiredPermissions.length > 0) {
|
|
75
|
-
const response = await requestPermission(ctx.
|
|
76
|
-
await publishPermissionResolved(ctx.nc, ctx.config, ctx.taskId, response);
|
|
74
|
+
const response = await requestPermission(ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
|
|
77
75
|
|
|
78
76
|
if (response === "aborted") {
|
|
79
77
|
await appendAndNotify(ctx, {
|
|
@@ -194,9 +192,7 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
194
192
|
|
|
195
193
|
// If requires_confirmation, notify clients and wait
|
|
196
194
|
if (task.frontmatter.requires_confirmation) {
|
|
197
|
-
const confirmed = await requestConfirmation(
|
|
198
|
-
const resolvedStatus = confirmed ? "confirmed" : "aborted";
|
|
199
|
-
await publishConfirmResolved(nc, config, taskId, resolvedStatus);
|
|
195
|
+
const confirmed = await requestConfirmation(config, task, taskDir);
|
|
200
196
|
if (!confirmed) {
|
|
201
197
|
console.log("Task aborted by user.");
|
|
202
198
|
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
|
|
@@ -275,7 +271,7 @@ async function runCommandTriggeredMode(
|
|
|
275
271
|
|
|
276
272
|
const child = spawnStreamingCommand(commandStr, {
|
|
277
273
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
278
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
|
|
274
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7400) },
|
|
279
275
|
});
|
|
280
276
|
|
|
281
277
|
let linesProcessed = 0;
|
|
@@ -408,41 +404,21 @@ async function publishTaskEvent(
|
|
|
408
404
|
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
409
405
|
}
|
|
410
406
|
|
|
411
|
-
/**
|
|
412
|
-
* Notify clients that a confirmation request has been resolved.
|
|
413
|
-
*/
|
|
414
|
-
async function publishConfirmResolved(
|
|
415
|
-
nc: NatsConnection | undefined,
|
|
416
|
-
config: HostConfig,
|
|
417
|
-
taskId: string,
|
|
418
|
-
status: "confirmed" | "aborted",
|
|
419
|
-
): Promise<void> {
|
|
420
|
-
await publishHostEvent(nc, config.hostId, taskId, {
|
|
421
|
-
event_type: "confirm-resolved",
|
|
422
|
-
host_id: config.hostId,
|
|
423
|
-
status,
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
407
|
|
|
427
408
|
async function requestPermission(
|
|
428
|
-
nc: NatsConnection | undefined,
|
|
429
409
|
config: HostConfig,
|
|
430
410
|
task: ParsedTask,
|
|
431
411
|
taskDir: string,
|
|
432
412
|
requiredPermissions: RequiredPermission[],
|
|
433
413
|
): Promise<"granted" | "granted_all" | "aborted"> {
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
host_id: config.hostId,
|
|
440
|
-
required_permissions: requiredPermissions,
|
|
441
|
-
name: task.frontmatter.name,
|
|
414
|
+
const port = config.httpPort ?? 7400;
|
|
415
|
+
const params = new URLSearchParams({
|
|
416
|
+
taskId: task.frontmatter.id,
|
|
417
|
+
taskName: task.frontmatter.name,
|
|
418
|
+
permissions: JSON.stringify(requiredPermissions),
|
|
442
419
|
});
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
const response = userInput[0] as "granted" | "granted_all" | "aborted";
|
|
420
|
+
const res = await fetch(`http://localhost:${port}/request-permission?${params}`);
|
|
421
|
+
const { response } = await res.json() as { response: "granted" | "granted_all" | "aborted" };
|
|
446
422
|
writeTaskStatus(taskDir, {
|
|
447
423
|
running_state: response === "aborted" ? "aborted" : "started",
|
|
448
424
|
time_stamp: Date.now(),
|
|
@@ -450,35 +426,19 @@ async function requestPermission(
|
|
|
450
426
|
return response;
|
|
451
427
|
}
|
|
452
428
|
|
|
453
|
-
async function publishPermissionResolved(
|
|
454
|
-
nc: NatsConnection | undefined,
|
|
455
|
-
config: HostConfig,
|
|
456
|
-
taskId: string,
|
|
457
|
-
status: "granted" | "granted_all" | "aborted",
|
|
458
|
-
): Promise<void> {
|
|
459
|
-
await publishHostEvent(nc, config.hostId, taskId, {
|
|
460
|
-
event_type: "permission-resolved",
|
|
461
|
-
host_id: config.hostId,
|
|
462
|
-
status,
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
429
|
|
|
466
430
|
async function requestConfirmation(
|
|
467
|
-
nc: NatsConnection | undefined,
|
|
468
431
|
config: HostConfig,
|
|
469
432
|
task: ParsedTask,
|
|
470
433
|
taskDir: string,
|
|
471
434
|
): Promise<boolean> {
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
event_type: "confirm-request",
|
|
477
|
-
host_id: config.hostId,
|
|
435
|
+
const port = config.httpPort ?? 7400;
|
|
436
|
+
const params = new URLSearchParams({
|
|
437
|
+
taskId: task.frontmatter.id,
|
|
438
|
+
taskName: task.frontmatter.name,
|
|
478
439
|
});
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
const confirmed = userInput[0] === "confirmed";
|
|
440
|
+
const res = await fetch(`http://localhost:${port}/request-confirmation?${params}`);
|
|
441
|
+
const { confirmed } = await res.json() as { confirmed: boolean };
|
|
482
442
|
writeTaskStatus(taskDir, {
|
|
483
443
|
running_state: confirmed ? "started" : "aborted",
|
|
484
444
|
time_stamp: Date.now(),
|
package/src/commands/serve.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { loadConfig } from "../config.js";
|
|
|
4
4
|
import { connectNats } from "../nats-client.js";
|
|
5
5
|
import { createRpcHandler } from "../rpc-handler.js";
|
|
6
6
|
import { startNatsTransport } from "../transports/nats-transport.js";
|
|
7
|
+
import { startHttpTransport } from "../transports/http-transport.js";
|
|
7
8
|
import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage } from "../task.js";
|
|
8
9
|
import { publishHostEvent } from "../events.js";
|
|
9
10
|
import { getPlatform } from "../platform/index.js";
|
|
@@ -78,7 +79,7 @@ async function checkStaleTasks(
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
|
-
* Start the persistent RPC handler (NATS
|
|
82
|
+
* Start the persistent RPC handler (NATS + HTTP).
|
|
82
83
|
*/
|
|
83
84
|
export async function serveCommand(): Promise<void> {
|
|
84
85
|
const config = loadConfig();
|
|
@@ -107,5 +108,13 @@ export async function serveCommand(): Promise<void> {
|
|
|
107
108
|
}, POLL_INTERVAL_MS);
|
|
108
109
|
|
|
109
110
|
const handleRpc = createRpcHandler(config, nc);
|
|
110
|
-
|
|
111
|
+
const httpPort = config.httpPort ?? 7400;
|
|
112
|
+
|
|
113
|
+
// Start NATS transport (loops forever, fire-and-forget)
|
|
114
|
+
if (nc) {
|
|
115
|
+
startNatsTransport(config, handleRpc, nc);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Start HTTP transport (loops forever)
|
|
119
|
+
await startHttpTransport(config, handleRpc, httpPort, nc);
|
|
111
120
|
}
|
package/src/events.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { StringCodec, type NatsConnection } from "nats";
|
|
2
|
-
import {
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
3
|
|
|
4
4
|
const sc = StringCodec();
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Broadcast an event to connected clients via NATS and HTTP SSE
|
|
7
|
+
* Broadcast an event to connected clients via NATS and HTTP SSE.
|
|
8
8
|
*
|
|
9
9
|
* - NATS: publishes to `host-event.{hostId}.{taskId}`
|
|
10
|
-
* - HTTP: POSTs to the
|
|
10
|
+
* - HTTP: POSTs to the serve daemon's `/event` endpoint
|
|
11
11
|
*/
|
|
12
12
|
export async function publishHostEvent(
|
|
13
13
|
nc: NatsConnection | undefined,
|
|
@@ -22,17 +22,16 @@ export async function publishHostEvent(
|
|
|
22
22
|
console.log(`[nats] ${subject} →`, payload);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
const port = config.httpPort ?? 7400;
|
|
27
|
+
try {
|
|
28
|
+
await fetch(`http://localhost:${port}/event`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
body: JSON.stringify({ task_id: taskId, ...payload }),
|
|
32
|
+
});
|
|
33
|
+
console.log(`[http] host-event: ${taskId} →`, payload);
|
|
34
|
+
} catch {
|
|
35
|
+
// Serve HTTP may not be ready yet — ignore
|
|
37
36
|
}
|
|
38
37
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,11 +9,8 @@ import { initCommand } from "./commands/init.js";
|
|
|
9
9
|
import { infoCommand } from "./commands/info.js";
|
|
10
10
|
import { runCommand } from "./commands/run.js";
|
|
11
11
|
import { serveCommand } from "./commands/serve.js";
|
|
12
|
-
import { notifyCommand } from "./commands/notify.js";
|
|
13
|
-
import { requestInputCommand } from "./commands/request-input.js";
|
|
14
12
|
|
|
15
13
|
import { pairCommand } from "./commands/pair.js";
|
|
16
|
-
import { lanCommand } from "./commands/lan.js";
|
|
17
14
|
import { restartCommand } from "./commands/restart.js";
|
|
18
15
|
import { sessionsListCommand, sessionsRevokeCommand, sessionsRevokeAllCommand } from "./commands/sessions.js";
|
|
19
16
|
|
|
@@ -62,22 +59,6 @@ program
|
|
|
62
59
|
await restartCommand();
|
|
63
60
|
});
|
|
64
61
|
|
|
65
|
-
program
|
|
66
|
-
.command("notify")
|
|
67
|
-
.description("Send a push notification to the user")
|
|
68
|
-
.requiredOption("--title <title>", "Notification title")
|
|
69
|
-
.requiredOption("--body <body>", "Notification body text")
|
|
70
|
-
.action(async (opts: { title: string; body: string }) => {
|
|
71
|
-
await notifyCommand(opts);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
program
|
|
75
|
-
.command("request-input")
|
|
76
|
-
.description("Request input from the user (requires PALMIER_TASK_ID env var)")
|
|
77
|
-
.requiredOption("--description <desc...>", "Input descriptions to show the user")
|
|
78
|
-
.action(async (opts: { description: string[] }) => {
|
|
79
|
-
await requestInputCommand(opts);
|
|
80
|
-
});
|
|
81
62
|
|
|
82
63
|
program
|
|
83
64
|
.command("pair")
|
|
@@ -86,13 +67,6 @@ program
|
|
|
86
67
|
await pairCommand();
|
|
87
68
|
});
|
|
88
69
|
|
|
89
|
-
program
|
|
90
|
-
.command("lan")
|
|
91
|
-
.description("Start an on-demand LAN server for direct HTTP connections")
|
|
92
|
-
.option("-p, --port <port>", "Port to listen on", "7400")
|
|
93
|
-
.action(async (opts: { port: string }) => {
|
|
94
|
-
await lanCommand({ port: parseInt(opts.port, 10) });
|
|
95
|
-
});
|
|
96
70
|
|
|
97
71
|
const sessionsCmd = program
|
|
98
72
|
.command("sessions")
|