palmier 0.4.2 → 0.4.3
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 +13 -27
- package/dist/agents/agent-instructions.md +40 -0
- package/dist/agents/claude.js +2 -8
- package/dist/agents/codex.js +0 -6
- package/dist/agents/copilot.js +0 -20
- package/dist/agents/gemini.js +0 -6
- package/dist/agents/shared-prompt.d.ts +1 -2
- package/dist/agents/shared-prompt.js +5 -18
- package/dist/commands/notify.d.ts +9 -0
- package/dist/commands/notify.js +43 -0
- package/dist/commands/request-input.d.ts +11 -0
- package/dist/commands/request-input.js +63 -0
- package/dist/commands/run.d.ts +0 -5
- package/dist/commands/run.js +67 -72
- package/dist/commands/serve.js +7 -2
- package/dist/index.js +15 -5
- package/dist/platform/windows.js +17 -2
- package/dist/rpc-handler.js +41 -25
- package/dist/spawn-command.d.ts +1 -1
- package/dist/spawn-command.js +13 -1
- package/dist/task.d.ts +12 -1
- package/dist/task.js +36 -1
- package/dist/types.d.ts +9 -0
- package/package.json +2 -3
- package/src/agents/agent-instructions.md +40 -0
- package/src/agents/claude.ts +2 -7
- package/src/agents/codex.ts +0 -5
- package/src/agents/copilot.ts +0 -19
- package/src/agents/gemini.ts +0 -5
- package/src/agents/shared-prompt.ts +10 -18
- package/src/commands/notify.ts +44 -0
- package/src/commands/request-input.ts +65 -0
- package/src/commands/run.ts +78 -96
- package/src/commands/serve.ts +7 -2
- package/src/index.ts +16 -5
- package/src/platform/windows.ts +17 -2
- package/src/rpc-handler.ts +50 -24
- package/src/spawn-command.ts +13 -2
- package/src/task.ts +47 -2
- package/src/types.ts +10 -0
- package/dist/commands/mcpserver.d.ts +0 -2
- package/dist/commands/mcpserver.js +0 -93
- package/src/commands/mcpserver.ts +0 -113
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**Website:** [palmier.me](https://www.palmier.me) | **App:** [app.palmier.me](https://app.palmier.me)
|
|
8
8
|
|
|
9
|
-
A Node.js CLI that lets you
|
|
9
|
+
A Node.js CLI that lets you dispatch your own AI agents from your phone. It runs on your machine as a persistent daemon, letting you create, schedule, and monitor agent tasks from any device via a cloud relay (NATS) and/or direct HTTP.
|
|
10
10
|
|
|
11
11
|
> **Important:** By using Palmier, you agree to the [Terms of Service](https://www.palmier.me/terms) and [Privacy Policy](https://www.palmier.me/privacy). See the [Disclaimer](#disclaimer) section below.
|
|
12
12
|
|
|
@@ -51,7 +51,8 @@ All `palmier` commands should be run from a dedicated Palmier root directory (e.
|
|
|
51
51
|
| `palmier serve` | Run the persistent RPC handler (default command) |
|
|
52
52
|
| `palmier restart` | Restart the palmier serve daemon |
|
|
53
53
|
| `palmier run <task-id>` | Execute a specific task |
|
|
54
|
-
| `palmier
|
|
54
|
+
| `palmier notify` | Send a push notification to paired devices |
|
|
55
|
+
| `palmier request-input` | Request input from the user during task execution |
|
|
55
56
|
|
|
56
57
|
## Setup
|
|
57
58
|
|
|
@@ -131,7 +132,7 @@ palmier restart
|
|
|
131
132
|
- **Task confirmation** — tasks can optionally require your approval before running. You'll get a push notification (server mode) or a prompt in the PWA to confirm or abort.
|
|
132
133
|
- **Run history** — each run produces a timestamped result file. You can view results and reports from the PWA.
|
|
133
134
|
- **Real-time updates** — task status changes (started, finished, failed) are pushed to connected PWA clients via NATS pub/sub (server mode) and/or SSE (LAN mode).
|
|
134
|
-
- **
|
|
135
|
+
- **Agent CLI commands** — `palmier notify` and `palmier request-input` allow agents to send push notifications and request user input during task execution without requiring MCP support.
|
|
135
136
|
|
|
136
137
|
## NATS Subjects
|
|
137
138
|
|
|
@@ -173,7 +174,8 @@ src/
|
|
|
173
174
|
serve.ts # Transport selection, startup, and crash detection polling
|
|
174
175
|
restart.ts # Daemon restart (cross-platform)
|
|
175
176
|
run.ts # Single task execution
|
|
176
|
-
|
|
177
|
+
notify.ts # Send push notification to paired devices
|
|
178
|
+
request-input.ts # Request user input during task execution
|
|
177
179
|
platform/
|
|
178
180
|
platform.ts # PlatformService interface
|
|
179
181
|
index.ts # Platform factory (Linux vs Windows)
|
|
@@ -184,32 +186,16 @@ src/
|
|
|
184
186
|
http-transport.ts # HTTP server with RPC, SSE, PWA reverse proxy, and internal event endpoints
|
|
185
187
|
```
|
|
186
188
|
|
|
187
|
-
##
|
|
189
|
+
## Agent CLI Commands
|
|
188
190
|
|
|
189
|
-
|
|
191
|
+
These commands are available to agents during task execution. They are included in the agent's system prompt automatically.
|
|
190
192
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
Add to your Claude Code MCP settings:
|
|
194
|
-
|
|
195
|
-
```json
|
|
196
|
-
{
|
|
197
|
-
"mcpServers": {
|
|
198
|
-
"palmier": {
|
|
199
|
-
"command": "palmier",
|
|
200
|
-
"args": ["mcpserver"]
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
Requires a provisioned host (`palmier init`) with server mode enabled.
|
|
207
|
-
|
|
208
|
-
### Available Tools
|
|
209
|
-
|
|
210
|
-
| Tool | Inputs | Description |
|
|
193
|
+
| Command | Flags | Description |
|
|
211
194
|
|---|---|---|
|
|
212
|
-
| `
|
|
195
|
+
| `palmier notify` | `--title <title>` `--body <body>` | Send a push notification to all paired devices |
|
|
196
|
+
| `palmier request-input` | `--description <desc...>` | Request input from the user; blocks until a response is provided |
|
|
197
|
+
|
|
198
|
+
Push notifications require server mode to be enabled. `request-input` requires the `PALMIER_TASK_ID` environment variable (set automatically during task execution).
|
|
213
199
|
|
|
214
200
|
## Uninstalling
|
|
215
201
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
You are an AI agent executing a task on behalf of the user via the Palmier platform. Follow these instructions carefully.
|
|
2
|
+
|
|
3
|
+
## Reporting Output
|
|
4
|
+
|
|
5
|
+
If you generate report or output files, print each file path on its own line prefixed with [PALMIER_REPORT]:
|
|
6
|
+
[PALMIER_REPORT] report.md
|
|
7
|
+
[PALMIER_REPORT] summary.md
|
|
8
|
+
|
|
9
|
+
## Completion
|
|
10
|
+
|
|
11
|
+
When you are done, output exactly one of these markers as the very last line:
|
|
12
|
+
- Success: [PALMIER_TASK_SUCCESS]
|
|
13
|
+
- Failure: [PALMIER_TASK_FAILURE]
|
|
14
|
+
Do not wrap them in code blocks or add text on the same line.
|
|
15
|
+
|
|
16
|
+
## Permissions
|
|
17
|
+
|
|
18
|
+
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]:
|
|
19
|
+
[PALMIER_PERMISSION] Read | Read file contents from the repository
|
|
20
|
+
[PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
|
|
21
|
+
[PALMIER_PERMISSION] Write | Write generated output files
|
|
22
|
+
|
|
23
|
+
## CLI Commands
|
|
24
|
+
|
|
25
|
+
You have access to the following palmier CLI commands:
|
|
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 it:
|
|
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.
|
|
32
|
+
|
|
33
|
+
**Sending push notifications** — If you need to send a push notification to the user:
|
|
34
|
+
```
|
|
35
|
+
palmier notify --title "Task Complete" --body "The deployment finished successfully."
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
The task to execute follows below.
|
package/dist/agents/claude.js
CHANGED
|
@@ -9,8 +9,8 @@ export class ClaudeAgent {
|
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
getTaskRunCommandLine(task, retryPrompt, extraPermissions) {
|
|
12
|
-
const prompt = retryPrompt ?? (task.body || task.frontmatter.user_prompt);
|
|
13
|
-
const args = ["--permission-mode", "acceptEdits", "
|
|
12
|
+
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
13
|
+
const args = ["--permission-mode", "acceptEdits", "-p"];
|
|
14
14
|
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
15
15
|
for (const p of allPerms) {
|
|
16
16
|
args.push("--allowedTools", p.name);
|
|
@@ -27,12 +27,6 @@ export class ClaudeAgent {
|
|
|
27
27
|
catch {
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
|
-
try {
|
|
31
|
-
execSync("claude mcp add --transport stdio palmier --scope user -- palmier mcpserver", { stdio: "ignore", shell: SHELL });
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
// MCP registration is best-effort; agent still works without it
|
|
35
|
-
}
|
|
36
30
|
return true;
|
|
37
31
|
}
|
|
38
32
|
}
|
package/dist/agents/codex.js
CHANGED
|
@@ -30,12 +30,6 @@ export class CodexAgent {
|
|
|
30
30
|
catch {
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
33
|
-
try {
|
|
34
|
-
execSync("codex mcp add palmier palmier mcpserver", { stdio: "ignore", shell: SHELL });
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
// MCP registration is best-effort; agent still works without it
|
|
38
|
-
}
|
|
39
33
|
return true;
|
|
40
34
|
}
|
|
41
35
|
}
|
package/dist/agents/copilot.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { homedir } from "os";
|
|
4
1
|
import { execSync } from "child_process";
|
|
5
2
|
import { AGENT_INSTRUCTIONS } from "./shared-prompt.js";
|
|
6
3
|
import { SHELL } from "../platform/index.js";
|
|
@@ -31,23 +28,6 @@ export class CopilotAgent {
|
|
|
31
28
|
catch {
|
|
32
29
|
return false;
|
|
33
30
|
}
|
|
34
|
-
// Register Palmier MCP server in ~/.copilot/mcp-config.json
|
|
35
|
-
try {
|
|
36
|
-
const configDir = path.join(homedir(), ".copilot");
|
|
37
|
-
const configFile = path.join(configDir, "mcp-config.json");
|
|
38
|
-
let config = {};
|
|
39
|
-
if (fs.existsSync(configFile)) {
|
|
40
|
-
config = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
41
|
-
}
|
|
42
|
-
const servers = (config.mcpServers ?? {});
|
|
43
|
-
servers.palmier = { command: "palmier", args: ["mcpserver"] };
|
|
44
|
-
config.mcpServers = servers;
|
|
45
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
46
|
-
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), "utf-8");
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
// MCP registration is best-effort
|
|
50
|
-
}
|
|
51
31
|
return true;
|
|
52
32
|
}
|
|
53
33
|
}
|
package/dist/agents/gemini.js
CHANGED
|
@@ -31,12 +31,6 @@ export class GeminiAgent {
|
|
|
31
31
|
catch {
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
|
-
try {
|
|
35
|
-
execSync("gemini mcp add --scope user palmier palmier mcpserver", { stdio: "ignore", shell: SHELL });
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
// MCP registration is best-effort; agent still works without it
|
|
39
|
-
}
|
|
40
34
|
return true;
|
|
41
35
|
}
|
|
42
36
|
}
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
* Instructs the agent to output structured markers so palmier can determine
|
|
4
4
|
* the task outcome, report files, and permission/input requests.
|
|
5
5
|
*/
|
|
6
|
-
export declare const AGENT_INSTRUCTIONS
|
|
6
|
+
export declare const AGENT_INSTRUCTIONS: string;
|
|
7
7
|
export declare const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
|
|
8
8
|
export declare const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
|
|
9
9
|
export declare const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
|
|
10
10
|
export declare const TASK_PERMISSION_PREFIX = "[PALMIER_PERMISSION]";
|
|
11
|
-
export declare const TASK_INPUT_PREFIX = "[PALMIER_INPUT]";
|
|
12
11
|
//# sourceMappingURL=shared-prompt.d.ts.map
|
|
@@ -1,28 +1,15 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1
5
|
/**
|
|
2
6
|
* Instructions prepended or injected as system prompt for every task invocation.
|
|
3
7
|
* Instructs the agent to output structured markers so palmier can determine
|
|
4
8
|
* the task outcome, report files, and permission/input requests.
|
|
5
9
|
*/
|
|
6
|
-
export const AGENT_INSTRUCTIONS =
|
|
7
|
-
[PALMIER_REPORT] report.md
|
|
8
|
-
[PALMIER_REPORT] summary.md
|
|
9
|
-
|
|
10
|
-
When you are done, output exactly one of these markers as the very last line:
|
|
11
|
-
- Success: [PALMIER_TASK_SUCCESS]
|
|
12
|
-
- Failure: [PALMIER_TASK_FAILURE]
|
|
13
|
-
Do not wrap them in code blocks or add text on the same line.
|
|
14
|
-
|
|
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.
|
|
16
|
-
[PALMIER_PERMISSION] Read | Read file contents from the repository
|
|
17
|
-
[PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
|
|
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?`;
|
|
10
|
+
export const AGENT_INSTRUCTIONS = fs.readFileSync(path.join(__dirname, "agent-instructions.md"), "utf-8");
|
|
23
11
|
export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
|
|
24
12
|
export const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
|
|
25
13
|
export const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
|
|
26
14
|
export const TASK_PERMISSION_PREFIX = "[PALMIER_PERMISSION]";
|
|
27
|
-
export const TASK_INPUT_PREFIX = "[PALMIER_INPUT]";
|
|
28
15
|
//# sourceMappingURL=shared-prompt.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { StringCodec } from "nats";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { connectNats } from "../nats-client.js";
|
|
4
|
+
/**
|
|
5
|
+
* Send a push notification to the user via NATS.
|
|
6
|
+
* Usage: palmier notify --title "Title" --body "Body text"
|
|
7
|
+
*/
|
|
8
|
+
export async function notifyCommand(opts) {
|
|
9
|
+
const config = loadConfig();
|
|
10
|
+
const nc = await connectNats(config);
|
|
11
|
+
if (!nc) {
|
|
12
|
+
console.error("Error: NATS connection required for push notifications.");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const sc = StringCodec();
|
|
16
|
+
const payload = {
|
|
17
|
+
hostId: config.hostId,
|
|
18
|
+
title: opts.title,
|
|
19
|
+
body: opts.body,
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
const subject = `host.${config.hostId}.push.send`;
|
|
23
|
+
const reply = await nc.request(subject, sc.encode(JSON.stringify(payload)), {
|
|
24
|
+
timeout: 15_000,
|
|
25
|
+
});
|
|
26
|
+
const result = JSON.parse(sc.decode(reply.data));
|
|
27
|
+
if (result.ok) {
|
|
28
|
+
console.log("Push notification sent successfully.");
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.error(`Failed to send push notification: ${result.error}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
console.error(`Error sending push notification: ${err}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await nc.drain();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=notify.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request input from the user and print responses to stdout.
|
|
3
|
+
* Usage: palmier request-input --description "Question 1" --description "Question 2"
|
|
4
|
+
*
|
|
5
|
+
* Requires PALMIER_TASK_ID environment variable to be set.
|
|
6
|
+
* Outputs each response on its own line: "description: value"
|
|
7
|
+
*/
|
|
8
|
+
export declare function requestInputCommand(opts: {
|
|
9
|
+
description: string[];
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=request-input.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { loadConfig } from "../config.js";
|
|
2
|
+
import { connectNats } from "../nats-client.js";
|
|
3
|
+
import { getTaskDir, parseTaskFile, appendResultMessage } from "../task.js";
|
|
4
|
+
import { requestUserInput, publishInputResolved } from "../user-input.js";
|
|
5
|
+
/**
|
|
6
|
+
* Request input from the user and print responses to stdout.
|
|
7
|
+
* Usage: palmier request-input --description "Question 1" --description "Question 2"
|
|
8
|
+
*
|
|
9
|
+
* Requires PALMIER_TASK_ID environment variable to be set.
|
|
10
|
+
* Outputs each response on its own line: "description: value"
|
|
11
|
+
*/
|
|
12
|
+
export async function requestInputCommand(opts) {
|
|
13
|
+
const taskId = process.env.PALMIER_TASK_ID;
|
|
14
|
+
if (!taskId) {
|
|
15
|
+
console.error("Error: PALMIER_TASK_ID environment variable is not set.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const config = loadConfig();
|
|
19
|
+
const nc = await connectNats(config);
|
|
20
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
21
|
+
const task = parseTaskFile(taskDir);
|
|
22
|
+
try {
|
|
23
|
+
const response = await requestUserInput(nc, config, taskId, task.frontmatter.name, taskDir, opts.description);
|
|
24
|
+
await publishInputResolved(nc, config, taskId, response === "aborted" ? "aborted" : "provided");
|
|
25
|
+
if (response === "aborted") {
|
|
26
|
+
// Write abort as user message if RESULT file is available
|
|
27
|
+
const resultFile = process.env.PALMIER_RESULT_FILE;
|
|
28
|
+
if (resultFile) {
|
|
29
|
+
appendResultMessage(taskDir, resultFile, {
|
|
30
|
+
role: "user",
|
|
31
|
+
time: Date.now(),
|
|
32
|
+
content: "Input request aborted.",
|
|
33
|
+
type: "input",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
console.error("User aborted the input request.");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
// Write user input as a conversation message
|
|
40
|
+
const resultFile = process.env.PALMIER_RESULT_FILE;
|
|
41
|
+
if (resultFile) {
|
|
42
|
+
const lines = opts.description.map((desc, i) => `**${desc}** ${response[i]}`);
|
|
43
|
+
appendResultMessage(taskDir, resultFile, {
|
|
44
|
+
role: "user",
|
|
45
|
+
time: Date.now(),
|
|
46
|
+
content: lines.join("\n"),
|
|
47
|
+
type: "input",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
for (let i = 0; i < opts.description.length; i++) {
|
|
51
|
+
console.log(response[i]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.error(`Error requesting user input: ${err}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (nc)
|
|
60
|
+
await nc.drain();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=request-input.js.map
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -13,11 +13,6 @@ export declare function parseReportFiles(output: string): string[];
|
|
|
13
13
|
* Looks for lines matching: [PALMIER_PERMISSION] <tool> | <description>
|
|
14
14
|
*/
|
|
15
15
|
export declare function parsePermissions(output: string): RequiredPermission[];
|
|
16
|
-
/**
|
|
17
|
-
* Extract user input requests from agent output.
|
|
18
|
-
* Looks for lines matching: [PALMIER_INPUT] <description>
|
|
19
|
-
*/
|
|
20
|
-
export declare function parseInputRequests(output: string): string[];
|
|
21
16
|
/**
|
|
22
17
|
* Parse the agent's output for success/failure markers.
|
|
23
18
|
* Falls back to "finished" if no marker is found.
|
package/dist/commands/run.js
CHANGED
|
@@ -4,25 +4,12 @@ import * as readline from "readline";
|
|
|
4
4
|
import { spawnCommand, spawnStreamingCommand } from "../spawn-command.js";
|
|
5
5
|
import { loadConfig } from "../config.js";
|
|
6
6
|
import { connectNats } from "../nats-client.js";
|
|
7
|
-
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createResultFile } from "../task.js";
|
|
7
|
+
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createResultFile, appendResultMessage, finalizeResultFrontmatter } from "../task.js";
|
|
8
8
|
import { getAgent } from "../agents/agent.js";
|
|
9
9
|
import { getPlatform } from "../platform/index.js";
|
|
10
|
-
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX
|
|
10
|
+
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
|
|
11
11
|
import { publishHostEvent } from "../events.js";
|
|
12
|
-
import { waitForUserInput
|
|
13
|
-
/**
|
|
14
|
-
* Write a time-stamped RESULT file with frontmatter.
|
|
15
|
-
* Always generated, even for abort/fail.
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* Update an existing result file with the final outcome.
|
|
19
|
-
*/
|
|
20
|
-
function finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, runningState, startTime, endTime, output, reportFiles, requiredPermissions) {
|
|
21
|
-
const reportLine = reportFiles.length > 0 ? `\nreport_files: ${reportFiles.join(", ")}` : "";
|
|
22
|
-
const permLines = requiredPermissions.map((p) => `\nrequired_permission: ${p.name} | ${p.description}`).join("");
|
|
23
|
-
const content = `---\ntask_name: ${taskName}\nrunning_state: ${runningState}\nstart_time: ${startTime}\nend_time: ${endTime}\ntask_file: ${taskSnapshotName}${reportLine}${permLines}\n---\n${output}`;
|
|
24
|
-
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
25
|
-
}
|
|
12
|
+
import { waitForUserInput } from "../user-input.js";
|
|
26
13
|
/**
|
|
27
14
|
* Invoke the agent CLI with a retry loop for permissions and user input.
|
|
28
15
|
*
|
|
@@ -37,7 +24,7 @@ async function invokeAgentWithRetry(ctx, invokeTask) {
|
|
|
37
24
|
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, retryPrompt, ctx.transientPermissions);
|
|
38
25
|
const result = await spawnCommand(command, args, {
|
|
39
26
|
cwd: ctx.taskDir,
|
|
40
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id },
|
|
27
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RESULT_FILE: ctx.resultFileName },
|
|
41
28
|
echoStdout: true,
|
|
42
29
|
resolveOnFailure: true,
|
|
43
30
|
stdin,
|
|
@@ -45,15 +32,35 @@ async function invokeAgentWithRetry(ctx, invokeTask) {
|
|
|
45
32
|
const outcome = result.exitCode !== 0 ? "failed" : parseTaskOutcome(result.output);
|
|
46
33
|
const reportFiles = parseReportFiles(result.output);
|
|
47
34
|
const requiredPermissions = parsePermissions(result.output);
|
|
35
|
+
// Append assistant message for this invocation
|
|
36
|
+
await appendAndNotify(ctx, {
|
|
37
|
+
role: "assistant",
|
|
38
|
+
time: Date.now(),
|
|
39
|
+
content: stripPalmierMarkers(result.output),
|
|
40
|
+
attachments: reportFiles.length > 0 ? reportFiles : undefined,
|
|
41
|
+
});
|
|
48
42
|
// Permission retry
|
|
49
43
|
if (outcome === "failed" && requiredPermissions.length > 0) {
|
|
50
44
|
const response = await requestPermission(ctx.nc, ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
|
|
51
45
|
await publishPermissionResolved(ctx.nc, ctx.config, ctx.taskId, response);
|
|
52
46
|
if (response === "aborted") {
|
|
53
|
-
|
|
47
|
+
await appendAndNotify(ctx, {
|
|
48
|
+
role: "user",
|
|
49
|
+
time: Date.now(),
|
|
50
|
+
content: "Permissions denied. Task aborted.",
|
|
51
|
+
type: "permission",
|
|
52
|
+
});
|
|
53
|
+
return { outcome: "failed" };
|
|
54
54
|
}
|
|
55
55
|
const newPerms = requiredPermissions.filter((rp) => !ctx.task.frontmatter.permissions?.some((ep) => ep.name === rp.name)
|
|
56
56
|
&& !ctx.transientPermissions.some((ep) => ep.name === rp.name));
|
|
57
|
+
// Append user message for permission grant
|
|
58
|
+
await appendAndNotify(ctx, {
|
|
59
|
+
role: "user",
|
|
60
|
+
time: Date.now(),
|
|
61
|
+
content: `Permissions granted: ${newPerms.map((p) => p.name).join(", ")}`,
|
|
62
|
+
type: "permission",
|
|
63
|
+
});
|
|
57
64
|
if (response === "granted_all") {
|
|
58
65
|
ctx.task.frontmatter.permissions = [...(ctx.task.frontmatter.permissions ?? []), ...newPerms];
|
|
59
66
|
invokeTask.frontmatter.permissions = ctx.task.frontmatter.permissions;
|
|
@@ -65,22 +72,23 @@ async function invokeAgentWithRetry(ctx, invokeTask) {
|
|
|
65
72
|
retryPrompt = "Permissions granted, please continue.";
|
|
66
73
|
continue;
|
|
67
74
|
}
|
|
68
|
-
// Input retry
|
|
69
|
-
const inputRequests = parseInputRequests(result.output);
|
|
70
|
-
if (outcome === "failed" && inputRequests.length > 0) {
|
|
71
|
-
const response = await requestUserInput(ctx.nc, ctx.config, ctx.taskId, ctx.task.frontmatter.name, ctx.taskDir, inputRequests);
|
|
72
|
-
await publishInputResolved(ctx.nc, ctx.config, ctx.taskId, response === "aborted" ? "aborted" : "provided");
|
|
73
|
-
if (response === "aborted") {
|
|
74
|
-
return { output: result.output, outcome: "failed", reportFiles, requiredPermissions };
|
|
75
|
-
}
|
|
76
|
-
const inputLines = inputRequests.map((desc, i) => `- ${desc} → ${response[i]}`).join("\n");
|
|
77
|
-
retryPrompt = `The user provided the following inputs:\n${inputLines}\nPlease continue with these values.`;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
75
|
// Normal completion (success or non-retryable failure)
|
|
81
|
-
return {
|
|
76
|
+
return { outcome };
|
|
82
77
|
}
|
|
83
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Strip [PALMIER_*] marker lines from agent output.
|
|
81
|
+
*/
|
|
82
|
+
function stripPalmierMarkers(output) {
|
|
83
|
+
return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Append a conversation message to the RESULT file and notify connected clients.
|
|
87
|
+
*/
|
|
88
|
+
async function appendAndNotify(ctx, msg) {
|
|
89
|
+
appendResultMessage(ctx.taskDir, ctx.resultFileName, msg);
|
|
90
|
+
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated" });
|
|
91
|
+
}
|
|
84
92
|
/**
|
|
85
93
|
* Find an existing RESULT file with running_state=started (created by the RPC handler).
|
|
86
94
|
*/
|
|
@@ -134,6 +142,9 @@ export async function runCommand(taskId) {
|
|
|
134
142
|
nc = await connectNats(config);
|
|
135
143
|
// Mark as started immediately
|
|
136
144
|
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, resultFileName);
|
|
145
|
+
// Status: started
|
|
146
|
+
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
147
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated" });
|
|
137
148
|
// If requires_confirmation, notify clients and wait
|
|
138
149
|
if (task.frontmatter.requires_confirmation) {
|
|
139
150
|
const confirmed = await requestConfirmation(nc, config, task, taskDir);
|
|
@@ -141,53 +152,58 @@ export async function runCommand(taskId) {
|
|
|
141
152
|
await publishConfirmResolved(nc, config, taskId, resolvedStatus);
|
|
142
153
|
if (!confirmed) {
|
|
143
154
|
console.log("Task aborted by user.");
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: "aborted" });
|
|
156
|
+
finalizeResultFrontmatter(taskDir, resultFileName, { end_time: Date.now(), running_state: "aborted" });
|
|
146
157
|
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, resultFileName);
|
|
147
158
|
await cleanup();
|
|
148
159
|
return;
|
|
149
160
|
}
|
|
150
161
|
console.log("Task confirmed by user.");
|
|
162
|
+
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: "confirmation" });
|
|
163
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated" });
|
|
151
164
|
}
|
|
152
165
|
// Shared invocation context
|
|
153
166
|
const guiEnv = getPlatform().getGuiEnv();
|
|
154
167
|
const agent = getAgent(task.frontmatter.agent);
|
|
155
168
|
const ctx = {
|
|
156
|
-
agent, task, taskDir, guiEnv, nc, config, taskId,
|
|
169
|
+
agent, task, taskDir, resultFileName, guiEnv, nc, config, taskId,
|
|
157
170
|
transientPermissions: [],
|
|
158
171
|
};
|
|
159
172
|
if (task.frontmatter.command) {
|
|
160
173
|
// Command-triggered mode
|
|
161
174
|
const result = await runCommandTriggeredMode(ctx);
|
|
162
175
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
163
|
-
|
|
176
|
+
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
177
|
+
finalizeResultFrontmatter(taskDir, resultFileName, { end_time: result.endTime, running_state: outcome });
|
|
164
178
|
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
165
179
|
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
166
180
|
}
|
|
167
181
|
else {
|
|
168
|
-
// Standard execution
|
|
182
|
+
// Standard execution — add user prompt as first message
|
|
183
|
+
await appendAndNotify(ctx, {
|
|
184
|
+
role: "user",
|
|
185
|
+
time: Date.now(),
|
|
186
|
+
content: task.body || task.frontmatter.user_prompt,
|
|
187
|
+
});
|
|
169
188
|
const result = await invokeAgentWithRetry(ctx, task);
|
|
170
189
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
171
|
-
|
|
190
|
+
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
191
|
+
finalizeResultFrontmatter(taskDir, resultFileName, { end_time: Date.now(), running_state: outcome });
|
|
172
192
|
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
173
|
-
if (result.reportFiles.length > 0) {
|
|
174
|
-
await publishHostEvent(nc, config.hostId, taskId, {
|
|
175
|
-
event_type: "report-generated",
|
|
176
|
-
name: taskName,
|
|
177
|
-
report_files: result.reportFiles,
|
|
178
|
-
running_state: outcome,
|
|
179
|
-
result_file: resultFileName,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
193
|
console.log(`Task ${taskId} completed.`);
|
|
183
194
|
}
|
|
184
195
|
}
|
|
185
196
|
catch (err) {
|
|
186
197
|
console.error(`Task ${taskId} failed:`, err);
|
|
187
|
-
const endTime = Date.now();
|
|
188
198
|
const outcome = resolveOutcome(taskDir, "failed");
|
|
189
199
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
190
|
-
|
|
200
|
+
appendResultMessage(taskDir, resultFileName, {
|
|
201
|
+
role: "assistant",
|
|
202
|
+
time: Date.now(),
|
|
203
|
+
content: errorMsg,
|
|
204
|
+
});
|
|
205
|
+
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
206
|
+
finalizeResultFrontmatter(taskDir, resultFileName, { end_time: Date.now(), running_state: outcome });
|
|
191
207
|
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
192
208
|
process.exitCode = 1;
|
|
193
209
|
}
|
|
@@ -256,7 +272,7 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
256
272
|
else {
|
|
257
273
|
invocationsFailed++;
|
|
258
274
|
}
|
|
259
|
-
appendLog(line,
|
|
275
|
+
appendLog(line, "", result.outcome);
|
|
260
276
|
}
|
|
261
277
|
async function drainQueue() {
|
|
262
278
|
if (processing)
|
|
@@ -312,19 +328,13 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
312
328
|
});
|
|
313
329
|
}
|
|
314
330
|
const endTime = Date.now();
|
|
315
|
-
|
|
316
|
-
`Command: ${commandStr}`,
|
|
317
|
-
`Exit code: ${exitCode}`,
|
|
318
|
-
`Lines processed: ${linesProcessed}`,
|
|
319
|
-
`Agent invocations succeeded: ${invocationsSucceeded}`,
|
|
320
|
-
`Agent invocations failed: ${invocationsFailed}`,
|
|
321
|
-
].join("\n");
|
|
322
|
-
return { outcome: "finished", endTime, output: summary };
|
|
331
|
+
return { outcome: "finished", endTime };
|
|
323
332
|
}
|
|
324
333
|
async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName, resultFile) {
|
|
325
334
|
writeTaskStatus(taskDir, {
|
|
326
335
|
running_state: eventType,
|
|
327
336
|
time_stamp: Date.now(),
|
|
337
|
+
...(eventType === "started" ? { pid: process.pid } : {}),
|
|
328
338
|
});
|
|
329
339
|
const payload = { event_type: "running-state", running_state: eventType };
|
|
330
340
|
if (taskName)
|
|
@@ -417,21 +427,6 @@ export function parsePermissions(output) {
|
|
|
417
427
|
}
|
|
418
428
|
return perms;
|
|
419
429
|
}
|
|
420
|
-
/**
|
|
421
|
-
* Extract user input requests from agent output.
|
|
422
|
-
* Looks for lines matching: [PALMIER_INPUT] <description>
|
|
423
|
-
*/
|
|
424
|
-
export function parseInputRequests(output) {
|
|
425
|
-
const regex = new RegExp(`^\\${TASK_INPUT_PREFIX}\\s+(.+)$`, "gm");
|
|
426
|
-
const inputs = [];
|
|
427
|
-
let match;
|
|
428
|
-
while ((match = regex.exec(output)) !== null) {
|
|
429
|
-
const desc = match[1].trim();
|
|
430
|
-
if (desc)
|
|
431
|
-
inputs.push(desc);
|
|
432
|
-
}
|
|
433
|
-
return inputs;
|
|
434
|
-
}
|
|
435
430
|
/**
|
|
436
431
|
* Parse the agent's output for success/failure markers.
|
|
437
432
|
* Falls back to "finished" if no marker is found.
|