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.
Files changed (43) hide show
  1. package/README.md +13 -27
  2. package/dist/agents/agent-instructions.md +40 -0
  3. package/dist/agents/claude.js +2 -8
  4. package/dist/agents/codex.js +0 -6
  5. package/dist/agents/copilot.js +0 -20
  6. package/dist/agents/gemini.js +0 -6
  7. package/dist/agents/shared-prompt.d.ts +1 -2
  8. package/dist/agents/shared-prompt.js +5 -18
  9. package/dist/commands/notify.d.ts +9 -0
  10. package/dist/commands/notify.js +43 -0
  11. package/dist/commands/request-input.d.ts +11 -0
  12. package/dist/commands/request-input.js +63 -0
  13. package/dist/commands/run.d.ts +0 -5
  14. package/dist/commands/run.js +67 -72
  15. package/dist/commands/serve.js +7 -2
  16. package/dist/index.js +15 -5
  17. package/dist/platform/windows.js +17 -2
  18. package/dist/rpc-handler.js +41 -25
  19. package/dist/spawn-command.d.ts +1 -1
  20. package/dist/spawn-command.js +13 -1
  21. package/dist/task.d.ts +12 -1
  22. package/dist/task.js +36 -1
  23. package/dist/types.d.ts +9 -0
  24. package/package.json +2 -3
  25. package/src/agents/agent-instructions.md +40 -0
  26. package/src/agents/claude.ts +2 -7
  27. package/src/agents/codex.ts +0 -5
  28. package/src/agents/copilot.ts +0 -19
  29. package/src/agents/gemini.ts +0 -5
  30. package/src/agents/shared-prompt.ts +10 -18
  31. package/src/commands/notify.ts +44 -0
  32. package/src/commands/request-input.ts +65 -0
  33. package/src/commands/run.ts +78 -96
  34. package/src/commands/serve.ts +7 -2
  35. package/src/index.ts +16 -5
  36. package/src/platform/windows.ts +17 -2
  37. package/src/rpc-handler.ts +50 -24
  38. package/src/spawn-command.ts +13 -2
  39. package/src/task.ts +47 -2
  40. package/src/types.ts +10 -0
  41. package/dist/commands/mcpserver.d.ts +0 -2
  42. package/dist/commands/mcpserver.js +0 -93
  43. 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 run 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.
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 mcpserver` | Start an MCP server exposing Palmier tools (stdio transport) |
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
- - **MCP server** (`palmier mcpserver`) exposes platform tools (e.g., `send-push-notification`) to AI agents like Claude Code over stdio.
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
- mcpserver.ts # MCP server with platform tools (send-push-notification)
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
- ## MCP Server
189
+ ## Agent CLI Commands
188
190
 
189
- The host includes an MCP server that exposes Palmier platform tools to AI agents like Claude Code.
191
+ These commands are available to agents during task execution. They are included in the agent's system prompt automatically.
190
192
 
191
- ### Setup
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
- | `send-push-notification` | `title`, `body` (required) | Send a push notification to all paired devices |
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.
@@ -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", "--append-system-prompt", AGENT_INSTRUCTIONS, "-p"];
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
  }
@@ -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
  }
@@ -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
  }
@@ -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 = "If you generate report or output files, print each file name on its own line prefixed with [PALMIER_REPORT]: e.g.\n[PALMIER_REPORT] report.md\n[PALMIER_REPORT] summary.md\n\nWhen you are done, output exactly one of these markers as the very last line:\n- Success: [PALMIER_TASK_SUCCESS]\n- Failure: [PALMIER_TASK_FAILURE]\nDo not wrap them in code blocks or add text on the same line.\n\nIf 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.\n[PALMIER_PERMISSION] Read | Read file contents from the repository\n[PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm\n[PALMIER_PERMISSION] Write | Write generated output files\n\nIf 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.\n[PALMIER_INPUT] What is the database connection string?\n[PALMIER_INPUT] What is the API key for the external service?";
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 = `If you generate report or output files, print each file name on its own line prefixed with [PALMIER_REPORT]: e.g.
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,9 @@
1
+ /**
2
+ * Send a push notification to the user via NATS.
3
+ * Usage: palmier notify --title "Title" --body "Body text"
4
+ */
5
+ export declare function notifyCommand(opts: {
6
+ title: string;
7
+ body: string;
8
+ }): Promise<void>;
9
+ //# sourceMappingURL=notify.d.ts.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
@@ -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.
@@ -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, TASK_INPUT_PREFIX } from "../agents/shared-prompt.js";
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, requestUserInput, publishInputResolved } from "../user-input.js";
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
- return { output: result.output, outcome: "failed", reportFiles, requiredPermissions };
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 { output: result.output, outcome, reportFiles, requiredPermissions };
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
- const endTime = Date.now();
145
- finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, "aborted", startTime, endTime, "", [], []);
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
- finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, outcome, startTime, result.endTime, result.output, [], []);
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
- finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, outcome, startTime, Date.now(), result.output, result.reportFiles, result.requiredPermissions);
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
- finalizeResultFile(taskDir, resultFileName, taskName, taskSnapshotName, outcome, startTime, endTime, errorMsg, [], []);
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, result.output, result.outcome);
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
- const summary = [
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.