palmier 0.5.5 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ The serve daemon always runs a local HTTP server. Three access modes are availab
22
22
 
23
23
  **Local mode** is always available. The PWA is served at `http://localhost:<port>` and works without pairing or internet. The daemon binds to `127.0.0.1` by default.
24
24
 
25
- **LAN mode** is enabled during `palmier init`. The daemon binds to `0.0.0.0` instead, making the PWA and API endpoints accessible from the local network at `http://<host-ip>:<port>`. Devices must pair via OTP to access. Push notifications are not available.
25
+ **LAN mode** can be enabled during `palmier init`. The daemon binds to `0.0.0.0` instead, making the PWA and API endpoints accessible from the local network at `http://<host-ip>:<port>`. Devices must pair via OTP to access. Push notifications are not available.
26
26
 
27
27
  **Server mode** relays communication through the Palmier cloud server (via [NATS](https://nats.io), a lightweight messaging system). All features including push notifications are available. The PWA is served over HTTPS. Server mode and LAN mode can be active at the same time.
28
28
 
@@ -12,8 +12,7 @@ export class CodexAgent {
12
12
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
13
13
  const yolo = extraPermissions === "yolo";
14
14
  const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
15
- // Using danger-full-access until workspace-write is fixed: https://github.com/openai/codex/issues/12572
16
- const args = ["exec", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
15
+ const args = ["exec", "--skip-git-repo-check", "--sandbox", yolo ? "danger-full-access" : "workspace-write"];
17
16
  if (!yolo) {
18
17
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
19
18
  for (const p of allPerms) {
@@ -7,10 +7,6 @@ export declare function stripPalmierMarkers(output: string): string;
7
7
  * Execute a task by ID.
8
8
  */
9
9
  export declare function runCommand(taskId: string): Promise<void>;
10
- /**
11
- * Extract report file names from agent output.
12
- * Looks for lines matching: [PALMIER_REPORT] <filename>
13
- */
14
10
  export declare function parseReportFiles(output: string): string[];
15
11
  /**
16
12
  * Extract required permissions from agent output.
@@ -70,6 +70,14 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
70
70
  }
71
71
  writer.end(reportFiles.length > 0 ? reportFiles : undefined);
72
72
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
73
+ if (reportFiles.length > 0) {
74
+ await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, {
75
+ event_type: "report-generated",
76
+ run_id: ctx.runId,
77
+ name: ctx.task.frontmatter.name,
78
+ report_files: reportFiles,
79
+ });
80
+ }
73
81
  // Permission handling — agent requested permissions
74
82
  if (requiredPermissions.length > 0) {
75
83
  const response = await requestPermission(ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
@@ -408,6 +416,7 @@ async function requestConfirmation(config, task, taskDir) {
408
416
  * Extract report file names from agent output.
409
417
  * Looks for lines matching: [PALMIER_REPORT] <filename>
410
418
  */
419
+ const ALLOWED_REPORT_EXT = [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
411
420
  export function parseReportFiles(output) {
412
421
  const regex = new RegExp(`^\\${TASK_REPORT_PREFIX}\\s+(.+)$`, "gm");
413
422
  const files = [];
@@ -415,8 +424,12 @@ export function parseReportFiles(output) {
415
424
  while ((match = regex.exec(output)) !== null) {
416
425
  const name = match[1].trim();
417
426
  // Skip placeholder examples echoed from the prompt (e.g. "<filename>")
418
- if (name && !name.startsWith("<"))
419
- files.push(name);
427
+ if (!name || name.startsWith("<"))
428
+ continue;
429
+ const ext = name.lastIndexOf(".") >= 0 ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
430
+ if (!ALLOWED_REPORT_EXT.includes(ext))
431
+ continue;
432
+ files.push(name);
420
433
  }
421
434
  return files;
422
435
  }
@@ -154,6 +154,17 @@ export function createRpcHandler(config, nc) {
154
154
  host_platform: process.platform,
155
155
  };
156
156
  }
157
+ case "task.get": {
158
+ const params = request.params;
159
+ const taskDir = getTaskDir(config.projectRoot, params.id);
160
+ try {
161
+ const task = parseTaskFile(taskDir);
162
+ return flattenTask(task);
163
+ }
164
+ catch {
165
+ return { error: "Task not found" };
166
+ }
167
+ }
157
168
  case "task.create": {
158
169
  const params = request.params;
159
170
  // Only generate a plan for longer prompts that benefit from it
@@ -497,11 +508,14 @@ export function createRpcHandler(config, nc) {
497
508
  if (!params.run_id || !Array.isArray(params.report_files) || params.report_files.length === 0) {
498
509
  return { error: "run_id and report_files are required" };
499
510
  }
511
+ const ALLOWED_EXT = [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
512
+ const IMAGE_EXT = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
500
513
  const reports = [];
501
514
  const runDir = path.join(config.projectRoot, "tasks", params.id, params.run_id);
502
515
  for (const file of params.report_files) {
503
- if (!file.endsWith(".md") && !file.endsWith(".txt")) {
504
- reports.push({ file, error: "must end with .md or .txt" });
516
+ const ext = path.extname(file).toLowerCase();
517
+ if (!ALLOWED_EXT.includes(ext)) {
518
+ reports.push({ file, error: `unsupported file type: ${ext}` });
505
519
  continue;
506
520
  }
507
521
  const basename = path.basename(file);
@@ -511,8 +525,15 @@ export function createRpcHandler(config, nc) {
511
525
  }
512
526
  const reportPath = path.join(runDir, basename);
513
527
  try {
514
- const content = fs.readFileSync(reportPath, "utf-8");
515
- reports.push({ file, content });
528
+ if (IMAGE_EXT.includes(ext)) {
529
+ const buf = fs.readFileSync(reportPath);
530
+ const mime = ext === ".svg" ? "image/svg+xml" : `image/${ext.slice(1).replace("jpg", "jpeg")}`;
531
+ reports.push({ file, data_url: `data:${mime};base64,${buf.toString("base64")}` });
532
+ }
533
+ else {
534
+ const content = fs.readFileSync(reportPath, "utf-8");
535
+ reports.push({ file, content });
536
+ }
516
537
  }
517
538
  catch {
518
539
  reports.push({ file, error: "Report file not found" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -16,8 +16,7 @@ export class CodexAgent implements AgentTool {
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
18
  const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
19
- // Using danger-full-access until workspace-write is fixed: https://github.com/openai/codex/issues/12572
20
- const args = ["exec", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
19
+ const args = ["exec", "--skip-git-repo-check", "--sandbox", yolo ? "danger-full-access" : "workspace-write"];
21
20
 
22
21
  if (!yolo) {
23
22
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
@@ -106,6 +106,15 @@ async function invokeAgentWithRetries(
106
106
  writer.end(reportFiles.length > 0 ? reportFiles : undefined);
107
107
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
108
108
 
109
+ if (reportFiles.length > 0) {
110
+ await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, {
111
+ event_type: "report-generated",
112
+ run_id: ctx.runId,
113
+ name: ctx.task.frontmatter.name,
114
+ report_files: reportFiles,
115
+ });
116
+ }
117
+
109
118
  // Permission handling — agent requested permissions
110
119
  if (requiredPermissions.length > 0) {
111
120
  const response = await requestPermission(ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
@@ -499,6 +508,8 @@ async function requestConfirmation(
499
508
  * Extract report file names from agent output.
500
509
  * Looks for lines matching: [PALMIER_REPORT] <filename>
501
510
  */
511
+ const ALLOWED_REPORT_EXT = [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
512
+
502
513
  export function parseReportFiles(output: string): string[] {
503
514
  const regex = new RegExp(`^\\${TASK_REPORT_PREFIX}\\s+(.+)$`, "gm");
504
515
  const files: string[] = [];
@@ -506,7 +517,10 @@ export function parseReportFiles(output: string): string[] {
506
517
  while ((match = regex.exec(output)) !== null) {
507
518
  const name = match[1].trim();
508
519
  // Skip placeholder examples echoed from the prompt (e.g. "<filename>")
509
- if (name && !name.startsWith("<")) files.push(name);
520
+ if (!name || name.startsWith("<")) continue;
521
+ const ext = name.lastIndexOf(".") >= 0 ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
522
+ if (!ALLOWED_REPORT_EXT.includes(ext)) continue;
523
+ files.push(name);
510
524
  }
511
525
  return files;
512
526
  }
@@ -182,6 +182,17 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
182
182
  };
183
183
  }
184
184
 
185
+ case "task.get": {
186
+ const params = request.params as { id: string };
187
+ const taskDir = getTaskDir(config.projectRoot, params.id);
188
+ try {
189
+ const task = parseTaskFile(taskDir);
190
+ return flattenTask(task);
191
+ } catch {
192
+ return { error: "Task not found" };
193
+ }
194
+ }
195
+
185
196
  case "task.create": {
186
197
  const params = request.params as {
187
198
  user_prompt: string;
@@ -577,11 +588,14 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
577
588
  if (!params.run_id || !Array.isArray(params.report_files) || params.report_files.length === 0) {
578
589
  return { error: "run_id and report_files are required" };
579
590
  }
580
- const reports: Array<{ file: string; content?: string; error?: string }> = [];
591
+ const ALLOWED_EXT = [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
592
+ const IMAGE_EXT = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
593
+ const reports: Array<{ file: string; content?: string; data_url?: string; error?: string }> = [];
581
594
  const runDir = path.join(config.projectRoot, "tasks", params.id, params.run_id);
582
595
  for (const file of params.report_files) {
583
- if (!file.endsWith(".md") && !file.endsWith(".txt")) {
584
- reports.push({ file, error: "must end with .md or .txt" });
596
+ const ext = path.extname(file).toLowerCase();
597
+ if (!ALLOWED_EXT.includes(ext)) {
598
+ reports.push({ file, error: `unsupported file type: ${ext}` });
585
599
  continue;
586
600
  }
587
601
  const basename = path.basename(file);
@@ -591,8 +605,14 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
591
605
  }
592
606
  const reportPath = path.join(runDir, basename);
593
607
  try {
594
- const content = fs.readFileSync(reportPath, "utf-8");
595
- reports.push({ file, content });
608
+ if (IMAGE_EXT.includes(ext)) {
609
+ const buf = fs.readFileSync(reportPath);
610
+ const mime = ext === ".svg" ? "image/svg+xml" : `image/${ext.slice(1).replace("jpg", "jpeg")}`;
611
+ reports.push({ file, data_url: `data:${mime};base64,${buf.toString("base64")}` });
612
+ } else {
613
+ const content = fs.readFileSync(reportPath, "utf-8");
614
+ reports.push({ file, content });
615
+ }
596
616
  } catch {
597
617
  reports.push({ file, error: "Report file not found" });
598
618
  }