palmier 0.4.5 → 0.4.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/README.md +29 -31
- package/dist/agents/agent-instructions.md +9 -9
- package/dist/agents/claude.js +3 -3
- package/dist/agents/codex.js +3 -3
- package/dist/agents/copilot.js +3 -6
- package/dist/agents/gemini.js +4 -5
- 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/plan-generation.md +12 -15
- package/dist/commands/run.js +23 -44
- 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 +18 -10
- package/dist/task.d.ts +1 -1
- package/dist/task.js +3 -2
- package/dist/transports/http-transport.d.ts +4 -2
- package/dist/transports/http-transport.js +218 -77
- package/dist/types.d.ts +7 -16
- package/package.json +1 -1
- package/src/agents/agent-instructions.md +9 -9
- package/src/agents/claude.ts +3 -3
- package/src/agents/codex.ts +3 -3
- package/src/agents/copilot.ts +3 -6
- package/src/agents/gemini.ts +5 -5
- 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/plan-generation.md +12 -15
- package/src/commands/run.ts +21 -58
- 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 +18 -11
- package/src/task.ts +3 -1
- package/src/transports/http-transport.ts +232 -133
- 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
|
@@ -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();
|
|
@@ -1,24 +1,21 @@
|
|
|
1
|
-
You are a task planning assistant. Given a task description, produce a Markdown execution plan for an agent.
|
|
1
|
+
You are a task planning assistant. Given a task description, produce a Markdown execution plan for an AI agent to follow. Do not execute any part of the plan yourself.
|
|
2
2
|
|
|
3
|
-
Output
|
|
3
|
+
## Output Format
|
|
4
|
+
|
|
5
|
+
Start with a YAML frontmatter block (no code fences), then the plan body:
|
|
4
6
|
|
|
5
7
|
---
|
|
6
|
-
task_name: <
|
|
8
|
+
task_name: <concise label, 3-6 words>
|
|
7
9
|
---
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
**Plan body:**
|
|
12
|
-
|
|
13
|
-
### 1. Goal
|
|
14
|
-
What the task accomplishes and the expected end state.
|
|
11
|
+
<plan body>
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
Numbered sequence of concrete, actionable steps. Include conditional branches where behavior may vary. Each step must be unambiguous.
|
|
13
|
+
## Plan Body Guidelines
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
If the task produces formatted output (report, email, etc.), specify structure, sections,
|
|
15
|
+
- Write a numbered sequence of concrete, actionable steps.
|
|
16
|
+
- If the task produces formatted output (report, email, summary, etc.), specify the structure, sections, and tone.
|
|
17
|
+
- When a step requires user input, simply state what information is needed from the user. Do not specify how to obtain it — the agent has its own tool for requesting user input.
|
|
18
|
+
- Relative times in the task description (e.g., "yesterday", "last week") refer to execution time, not plan generation time.
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
## Task Description
|
|
23
21
|
|
|
24
|
-
**Task description:**
|
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, {
|
|
@@ -174,7 +172,7 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
174
172
|
|
|
175
173
|
// Use existing run dir if just created by RPC, otherwise create a new one
|
|
176
174
|
const existingRunId = findLatestPendingRunId(taskDir);
|
|
177
|
-
const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now());
|
|
175
|
+
const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now(), task.frontmatter.agent);
|
|
178
176
|
if (!existingRunId) {
|
|
179
177
|
appendHistory(config.projectRoot, { task_id: taskId, run_id: runId });
|
|
180
178
|
}
|
|
@@ -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,24 @@ 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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
414
|
+
const port = config.httpPort ?? 7400;
|
|
415
|
+
const res = await fetch(`http://localhost:${port}/request-permission`, {
|
|
416
|
+
method: "POST",
|
|
417
|
+
headers: { "Content-Type": "application/json" },
|
|
418
|
+
body: JSON.stringify({
|
|
419
|
+
taskId: task.frontmatter.id,
|
|
420
|
+
taskName: task.frontmatter.name,
|
|
421
|
+
permissions: requiredPermissions,
|
|
422
|
+
}),
|
|
442
423
|
});
|
|
443
|
-
|
|
444
|
-
const userInput = await waitForUserInput(taskDir);
|
|
445
|
-
const response = userInput[0] as "granted" | "granted_all" | "aborted";
|
|
424
|
+
const { response } = await res.json() as { response: "granted" | "granted_all" | "aborted" };
|
|
446
425
|
writeTaskStatus(taskDir, {
|
|
447
426
|
running_state: response === "aborted" ? "aborted" : "started",
|
|
448
427
|
time_stamp: Date.now(),
|
|
@@ -450,35 +429,19 @@ async function requestPermission(
|
|
|
450
429
|
return response;
|
|
451
430
|
}
|
|
452
431
|
|
|
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
432
|
|
|
466
433
|
async function requestConfirmation(
|
|
467
|
-
nc: NatsConnection | undefined,
|
|
468
434
|
config: HostConfig,
|
|
469
435
|
task: ParsedTask,
|
|
470
436
|
taskDir: string,
|
|
471
437
|
): Promise<boolean> {
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
host_id: config.hostId,
|
|
438
|
+
const port = config.httpPort ?? 7400;
|
|
439
|
+
const res = await fetch(`http://localhost:${port}/request-confirmation`, {
|
|
440
|
+
method: "POST",
|
|
441
|
+
headers: { "Content-Type": "application/json" },
|
|
442
|
+
body: JSON.stringify({ taskId: task.frontmatter.id, taskName: task.frontmatter.name }),
|
|
478
443
|
});
|
|
479
|
-
|
|
480
|
-
const userInput = await waitForUserInput(taskDir);
|
|
481
|
-
const confirmed = userInput[0] === "confirmed";
|
|
444
|
+
const { confirmed } = await res.json() as { confirmed: boolean };
|
|
482
445
|
writeTaskStatus(taskDir, {
|
|
483
446
|
running_state: confirmed ? "started" : "aborted",
|
|
484
447
|
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")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { RequiredPermission } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface PendingRequest {
|
|
4
|
+
type: "confirmation" | "permission" | "input";
|
|
5
|
+
resolve: (value: string[]) => void;
|
|
6
|
+
/** Permission list (for 'permission') or input descriptions (for 'input'). */
|
|
7
|
+
params?: RequiredPermission[] | string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const pending = new Map<string, PendingRequest>();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register a pending request for a task. Returns a Promise that resolves
|
|
14
|
+
* when `resolvePending` is called with the user's response.
|
|
15
|
+
* Only one pending request per task at a time.
|
|
16
|
+
*/
|
|
17
|
+
export function registerPending(
|
|
18
|
+
taskId: string,
|
|
19
|
+
type: PendingRequest["type"],
|
|
20
|
+
params?: PendingRequest["params"],
|
|
21
|
+
): Promise<string[]> {
|
|
22
|
+
if (pending.has(taskId)) {
|
|
23
|
+
return Promise.reject(new Error(`Task ${taskId} already has a pending request`));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return new Promise<string[]>((resolve) => {
|
|
27
|
+
pending.set(taskId, { type, resolve, params });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a pending request with the user's response.
|
|
33
|
+
* Returns true if a pending request was found and resolved.
|
|
34
|
+
*/
|
|
35
|
+
export function resolvePending(taskId: string, value: string[]): boolean {
|
|
36
|
+
const entry = pending.get(taskId);
|
|
37
|
+
if (!entry) return false;
|
|
38
|
+
pending.delete(taskId);
|
|
39
|
+
entry.resolve(value);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the current pending request for a task (if any).
|
|
45
|
+
*/
|
|
46
|
+
export function getPending(taskId: string): PendingRequest | undefined {
|
|
47
|
+
return pending.get(taskId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Remove a pending request without resolving it.
|
|
52
|
+
*/
|
|
53
|
+
export function removePending(taskId: string): void {
|
|
54
|
+
pending.delete(taskId);
|
|
55
|
+
}
|
package/src/rpc-handler.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { spawn, type ChildProcess } from "child_process";
|
|
|
6
6
|
import { parse as parseYaml } from "yaml";
|
|
7
7
|
import { type NatsConnection } from "nats";
|
|
8
8
|
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
|
|
9
|
+
import { resolvePending, getPending } from "./pending-requests.js";
|
|
9
10
|
import { getPlatform } from "./platform/index.js";
|
|
10
11
|
import { spawnCommand } from "./spawn-command.js";
|
|
11
12
|
import crossSpawn from "cross-spawn";
|
|
@@ -57,6 +58,7 @@ function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
|
57
58
|
return {
|
|
58
59
|
messages,
|
|
59
60
|
task_name: meta.task_name,
|
|
61
|
+
agent: meta.agent,
|
|
60
62
|
running_state: runningState,
|
|
61
63
|
start_time: startedMsg?.time || undefined,
|
|
62
64
|
end_time: terminalMsg?.time || undefined,
|
|
@@ -157,8 +159,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
async function handleRpc(request: RpcMessage): Promise<unknown> {
|
|
160
|
-
// Session token validation:
|
|
161
|
-
if (!request.sessionToken || !validateSession(request.sessionToken)) {
|
|
162
|
+
// Session token validation: skip for trusted localhost requests
|
|
163
|
+
if (!request.localhost && (!request.sessionToken || !validateSession(request.sessionToken))) {
|
|
162
164
|
return { error: "Unauthorized" };
|
|
163
165
|
}
|
|
164
166
|
|
|
@@ -317,7 +319,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
317
319
|
// Do NOT append to tasks.jsonl — this is a one-off run
|
|
318
320
|
|
|
319
321
|
// Create initial result file so it appears in runs list immediately
|
|
320
|
-
const runId = createRunDir(taskDir, name, Date.now());
|
|
322
|
+
const runId = createRunDir(taskDir, name, Date.now(), params.agent);
|
|
321
323
|
appendHistory(config.projectRoot, { task_id: id, run_id: runId });
|
|
322
324
|
|
|
323
325
|
// Spawn `palmier run <id>` directly as a detached process
|
|
@@ -338,7 +340,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
338
340
|
// Create initial result file so it appears in runs list immediately
|
|
339
341
|
const runTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
340
342
|
const runTask = parseTaskFile(runTaskDir);
|
|
341
|
-
const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now());
|
|
343
|
+
const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now(), runTask.frontmatter.agent);
|
|
342
344
|
appendHistory(config.projectRoot, { task_id: params.id, run_id: taskRunId });
|
|
343
345
|
|
|
344
346
|
await getPlatform().startTask(params.id);
|
|
@@ -521,7 +523,14 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
521
523
|
if (!status) {
|
|
522
524
|
return { task_id: params.id, error: "No status found" };
|
|
523
525
|
}
|
|
524
|
-
|
|
526
|
+
const pending = getPending(params.id);
|
|
527
|
+
return {
|
|
528
|
+
task_id: params.id,
|
|
529
|
+
...status,
|
|
530
|
+
...(pending?.type === "confirmation" ? { pending_confirmation: true } : {}),
|
|
531
|
+
...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
|
|
532
|
+
...(pending?.type === "input" ? { pending_input: pending.params } : {}),
|
|
533
|
+
};
|
|
525
534
|
}
|
|
526
535
|
|
|
527
536
|
case "task.result": {
|
|
@@ -570,17 +579,15 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
570
579
|
|
|
571
580
|
case "task.user_input": {
|
|
572
581
|
const params = request.params as { id: string; value: string[] };
|
|
573
|
-
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
574
582
|
|
|
575
|
-
const
|
|
576
|
-
if (!
|
|
583
|
+
const pending = getPending(params.id);
|
|
584
|
+
if (!pending) {
|
|
577
585
|
return { ok: false, error: "not pending" };
|
|
578
586
|
}
|
|
579
587
|
|
|
580
|
-
|
|
581
|
-
|
|
588
|
+
const resolved = resolvePending(params.id, params.value);
|
|
582
589
|
console.log(`[task.user_input] ${params.id} → ${params.value}`);
|
|
583
|
-
return { ok:
|
|
590
|
+
return { ok: resolved };
|
|
584
591
|
}
|
|
585
592
|
|
|
586
593
|
case "taskrun.list": {
|
package/src/task.ts
CHANGED
|
@@ -155,11 +155,13 @@ export function createRunDir(
|
|
|
155
155
|
taskDir: string,
|
|
156
156
|
taskName: string,
|
|
157
157
|
startTime: number,
|
|
158
|
+
agent?: string,
|
|
158
159
|
): string {
|
|
159
160
|
const runId = String(startTime);
|
|
160
161
|
const runDir = path.join(taskDir, runId);
|
|
161
162
|
fs.mkdirSync(runDir, { recursive: true });
|
|
162
|
-
const
|
|
163
|
+
const agentLine = agent ? `\nagent: ${agent}` : "";
|
|
164
|
+
const content = `---\ntask_name: ${taskName}${agentLine}\n---\n\n`;
|
|
163
165
|
fs.writeFileSync(path.join(runDir, "TASKRUN.md"), content, "utf-8");
|
|
164
166
|
return runId;
|
|
165
167
|
}
|