palmier 0.5.1 → 0.5.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 (70) hide show
  1. package/README.md +9 -9
  2. package/dist/agents/agent-instructions.md +7 -11
  3. package/dist/agents/agent.d.ts +8 -3
  4. package/dist/agents/agent.js +7 -1
  5. package/dist/agents/claude.d.ts +2 -1
  6. package/dist/agents/claude.js +10 -5
  7. package/dist/agents/codex.d.ts +2 -1
  8. package/dist/agents/codex.js +10 -6
  9. package/dist/agents/copilot.d.ts +2 -1
  10. package/dist/agents/copilot.js +10 -3
  11. package/dist/agents/gemini.d.ts +2 -1
  12. package/dist/agents/gemini.js +11 -7
  13. package/dist/agents/kimi.d.ts +9 -0
  14. package/dist/agents/kimi.js +35 -0
  15. package/dist/agents/openclaw.d.ts +2 -1
  16. package/dist/agents/openclaw.js +3 -1
  17. package/dist/agents/qwen.d.ts +9 -0
  18. package/dist/agents/qwen.js +32 -0
  19. package/dist/agents/shared-prompt.d.ts +1 -1
  20. package/dist/agents/shared-prompt.js +7 -3
  21. package/dist/client-store.d.ts +12 -0
  22. package/dist/client-store.js +57 -0
  23. package/dist/commands/clients.d.ts +4 -0
  24. package/dist/commands/clients.js +27 -0
  25. package/dist/commands/info.js +5 -5
  26. package/dist/commands/init.js +1 -1
  27. package/dist/commands/pair.js +4 -4
  28. package/dist/commands/run.js +21 -8
  29. package/dist/commands/serve.js +1 -1
  30. package/dist/events.js +1 -1
  31. package/dist/index.js +13 -13
  32. package/dist/rpc-handler.js +13 -6
  33. package/dist/task.d.ts +13 -3
  34. package/dist/task.js +39 -7
  35. package/dist/transports/http-transport.js +30 -13
  36. package/dist/transports/nats-transport.js +4 -4
  37. package/dist/types.d.ts +3 -2
  38. package/package.json +1 -1
  39. package/src/agents/agent-instructions.md +7 -11
  40. package/src/agents/agent.ts +16 -4
  41. package/src/agents/claude.ts +11 -6
  42. package/src/agents/codex.ts +11 -7
  43. package/src/agents/copilot.ts +10 -4
  44. package/src/agents/gemini.ts +12 -8
  45. package/src/agents/kimi.ts +37 -0
  46. package/src/agents/openclaw.ts +4 -2
  47. package/src/agents/qwen.ts +34 -0
  48. package/src/agents/shared-prompt.ts +7 -3
  49. package/src/client-store.ts +68 -0
  50. package/src/commands/clients.ts +29 -0
  51. package/src/commands/info.ts +5 -5
  52. package/src/commands/init.ts +1 -1
  53. package/src/commands/pair.ts +4 -4
  54. package/src/commands/run.ts +22 -8
  55. package/src/commands/serve.ts +1 -1
  56. package/src/events.ts +1 -1
  57. package/src/index.ts +13 -13
  58. package/src/rpc-handler.ts +15 -6
  59. package/src/task.ts +43 -8
  60. package/src/transports/http-transport.ts +32 -13
  61. package/src/transports/nats-transport.ts +4 -4
  62. package/src/types.ts +4 -3
  63. package/test/agent-instructions.test.ts +48 -0
  64. package/test/agent-output-parsing.test.ts +12 -0
  65. package/dist/commands/sessions.d.ts +0 -4
  66. package/dist/commands/sessions.js +0 -27
  67. package/dist/session-store.d.ts +0 -12
  68. package/dist/session-store.js +0 -57
  69. package/src/commands/sessions.ts +0 -29
  70. package/src/session-store.ts +0 -68
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CopilotAgent implements AgentTool {
8
+ supportsPermissions = false;
8
9
  getPlanGenerationCommandLine(prompt: string): CommandLine {
9
10
  return {
10
11
  command: "copilot",
@@ -12,12 +13,17 @@ export class CopilotAgent implements AgentTool {
12
13
  };
13
14
  }
14
15
 
15
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
19
  const args = ["-p", prompt];
18
20
 
19
- const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
- args.push(`--allow-tool=${["web_fetch", ...allPerms.map((p) => p.name)].join(",")}`);
21
+ if (yolo) {
22
+ args.push("--yolo");
23
+ } else {
24
+ const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
25
+ args.push(`--allow-tool=${["web_fetch", ...allPerms.map((p) => p.name)].join(",")}`);
26
+ }
21
27
  if (followupPrompt) { args.push("--continue"); }
22
28
  return { command: "copilot", args};
23
29
  }
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class GeminiAgent implements AgentTool {
8
+ supportsPermissions = true;
8
9
  getPlanGenerationCommandLine(prompt: string): CommandLine {
9
10
  return {
10
11
  command: "gemini",
@@ -12,21 +13,24 @@ export class GeminiAgent implements AgentTool {
12
13
  };
13
14
  }
14
15
 
15
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
- const args = ["--approval-mode", "auto_edit", "--allowed-tools", "web_fetch"];
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
19
+ const args = ["--approval-mode", yolo ? "yolo" : "auto_edit"];
18
20
 
19
- const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
- if (allPerms.length > 0) {
21
+ if (!yolo) {
22
+ const tools = ["run_shell_command", "web_fetch"];
23
+ const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
21
24
  for (const p of allPerms) {
22
- args.push(p.name);
25
+ tools.push(p.name);
23
26
  }
27
+ args.push("--allowed-tools", tools.join(","));
24
28
  }
25
29
 
26
30
  if (followupPrompt) {args.push("--resume");} // continue mode for followups
27
- args.push("--prompt", prompt);
31
+ args.push("--prompt", "-"); // read prompt from stdin to avoid command line length limits
28
32
 
29
- return { command: "gemini", args };
33
+ return { command: "gemini", args, stdin: prompt };
30
34
  }
31
35
 
32
36
  async init(): Promise<boolean> {
@@ -0,0 +1,37 @@
1
+ import type { ParsedTask, RequiredPermission } from "../types.js";
2
+ import { execSync } from "child_process";
3
+ import type { AgentTool, CommandLine } from "./agent.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
+ import { SHELL } from "../platform/index.js";
6
+
7
+ export class KimiAgent implements AgentTool {
8
+ supportsPermissions = false;
9
+ getPlanGenerationCommandLine(prompt: string): CommandLine {
10
+ return {
11
+ command: "kimi",
12
+ args: ["-p", prompt],
13
+ };
14
+ }
15
+
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
19
+ const args = [];
20
+
21
+ if (yolo) {
22
+ args.push("--yolo");
23
+ }
24
+ if (followupPrompt) { args.push("--continue"); }
25
+ args.push("-p", prompt);
26
+ return { command: "kimi", args };
27
+ }
28
+
29
+ async init(): Promise<boolean> {
30
+ try {
31
+ execSync("kimi --version", { stdio: "ignore", shell: SHELL });
32
+ } catch {
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ }
@@ -4,6 +4,7 @@ import type { AgentTool, CommandLine } from "./agent.js";
4
4
  import { getAgentInstructions } from "./shared-prompt.js";
5
5
 
6
6
  export class OpenClawAgent implements AgentTool {
7
+ supportsPermissions = false;
7
8
  getPlanGenerationCommandLine(prompt: string): CommandLine {
8
9
  return {
9
10
  command: "openclaw",
@@ -11,8 +12,9 @@ export class OpenClawAgent implements AgentTool {
11
12
  };
12
13
  }
13
14
 
14
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
15
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
15
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
16
+ const yolo = extraPermissions === "yolo";
17
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
16
18
  // OpenClaw does not support stdin as prompt.
17
19
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
18
20
 
@@ -0,0 +1,34 @@
1
+ import type { ParsedTask, RequiredPermission } from "../types.js";
2
+ import { execSync } from "child_process";
3
+ import type { AgentTool, CommandLine } from "./agent.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
+ import { SHELL } from "../platform/index.js";
6
+
7
+ export class QwenAgent implements AgentTool {
8
+ supportsPermissions = false;
9
+ getPlanGenerationCommandLine(prompt: string): CommandLine {
10
+ return {
11
+ command: "qwen",
12
+ args: ["-p", prompt],
13
+ };
14
+ }
15
+
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
19
+ const args = ["--approval-mode", yolo ? "yolo" : "auto-edit"];
20
+
21
+ if (followupPrompt) { args.push("-c"); }
22
+ args.push("-p", prompt);
23
+ return { command: "qwen", args };
24
+ }
25
+
26
+ async init(): Promise<boolean> {
27
+ try {
28
+ execSync("qwen --version", { stdio: "ignore", shell: SHELL });
29
+ } catch {
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+ }
@@ -13,11 +13,15 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
13
13
  /**
14
14
  * Agent instructions with the serve daemon's HTTP port and task ID baked in.
15
15
  */
16
- export function getAgentInstructions(taskId: string): string {
17
- const port = loadConfig().httpPort ?? 7400;
18
- return AGENT_INSTRUCTIONS_TEMPLATE
16
+ export function getAgentInstructions(taskId: string, skipPermissions?: boolean): string {
17
+ const port = loadConfig().httpPort ?? 9966;
18
+ let instructions = AGENT_INSTRUCTIONS_TEMPLATE
19
19
  .replace(/\{\{PORT\}\}/g, String(port))
20
20
  .replace(/\{\{TASK_ID\}\}/g, taskId);
21
+ if (skipPermissions) {
22
+ instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
23
+ }
24
+ return instructions;
21
25
  }
22
26
 
23
27
  export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
@@ -0,0 +1,68 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { randomBytes } from "crypto";
4
+ import { CONFIG_DIR } from "./config.js";
5
+
6
+ const CLIENTS_FILE = path.join(CONFIG_DIR, "clients.json");
7
+
8
+ export interface ClientEntry {
9
+ token: string;
10
+ createdAt: string;
11
+ label?: string;
12
+ }
13
+
14
+ function readFile(): ClientEntry[] {
15
+ try {
16
+ if (!fs.existsSync(CLIENTS_FILE)) return [];
17
+ const raw = fs.readFileSync(CLIENTS_FILE, "utf-8");
18
+ return JSON.parse(raw) as ClientEntry[];
19
+ } catch {
20
+ return [];
21
+ }
22
+ }
23
+
24
+ function writeFile(clients: ClientEntry[]): void {
25
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
26
+ fs.writeFileSync(CLIENTS_FILE, JSON.stringify(clients, null, 2), "utf-8");
27
+ }
28
+
29
+ export function loadClients(): ClientEntry[] {
30
+ return readFile();
31
+ }
32
+
33
+ export function addClient(label?: string): ClientEntry {
34
+ const clients = readFile();
35
+ const entry: ClientEntry = {
36
+ token: randomBytes(32).toString("hex"),
37
+ createdAt: new Date().toISOString(),
38
+ ...(label ? { label } : {}),
39
+ };
40
+ clients.push(entry);
41
+ writeFile(clients);
42
+ return entry;
43
+ }
44
+
45
+ export function revokeClient(token: string): boolean {
46
+ const clients = readFile();
47
+ const idx = clients.findIndex((c) => c.token === token);
48
+ if (idx === -1) return false;
49
+ clients.splice(idx, 1);
50
+ writeFile(clients);
51
+ return true;
52
+ }
53
+
54
+ export function revokeAllClients(): number {
55
+ const clients = readFile();
56
+ const count = clients.length;
57
+ writeFile([]);
58
+ return count;
59
+ }
60
+
61
+ export function validateClient(token: string): boolean {
62
+ const clients = readFile();
63
+ return clients.some((c) => c.token === token);
64
+ }
65
+
66
+ export function hasClients(): boolean {
67
+ return readFile().length > 0;
68
+ }
@@ -0,0 +1,29 @@
1
+ import { loadClients, revokeClient, revokeAllClients } from "../client-store.js";
2
+
3
+ export async function clientsListCommand(): Promise<void> {
4
+ const clients = loadClients();
5
+ if (clients.length === 0) {
6
+ console.log("No active clients.");
7
+ return;
8
+ }
9
+
10
+ console.log(`${clients.length} active client(s):\n`);
11
+ for (const c of clients) {
12
+ const label = c.label ? ` (${c.label})` : "";
13
+ console.log(` ${c.token}${label} created ${c.createdAt}`);
14
+ }
15
+ }
16
+
17
+ export async function clientsRevokeCommand(token: string): Promise<void> {
18
+ if (revokeClient(token)) {
19
+ console.log("Client revoked.");
20
+ } else {
21
+ console.error("Client not found.");
22
+ process.exit(1);
23
+ }
24
+ }
25
+
26
+ export async function clientsRevokeAllCommand(): Promise<void> {
27
+ const count = revokeAllClients();
28
+ console.log(`Revoked ${count} client(s).`);
29
+ }
@@ -1,12 +1,12 @@
1
1
  import { loadConfig } from "../config.js";
2
- import { loadSessions } from "../session-store.js";
2
+ import { loadClients } from "../client-store.js";
3
3
 
4
4
  /**
5
5
  * Print host connection info for setting up clients.
6
6
  */
7
7
  export async function infoCommand(): Promise<void> {
8
8
  const config = loadConfig();
9
- const sessions = loadSessions();
9
+ const clients = loadClients();
10
10
 
11
11
  console.log(`Host ID: ${config.hostId}`);
12
12
  console.log(`Project root: ${config.projectRoot}`);
@@ -18,10 +18,10 @@ export async function infoCommand(): Promise<void> {
18
18
  console.log(`Agents: (none detected — run \`palmier agents\`)`);
19
19
  }
20
20
 
21
- // Sessions
22
- console.log(`Sessions: ${sessions.length} active`);
21
+ // Clients
22
+ console.log(`Clients: ${clients.length} active`);
23
23
 
24
- if (sessions.length === 0) {
24
+ if (clients.length === 0) {
25
25
  console.log("");
26
26
  console.log("No paired clients. Run `palmier pair` to connect a device.");
27
27
  }
@@ -44,7 +44,7 @@ export async function initCommand(): Promise<void> {
44
44
  const lanAnswer = await ask("Enable LAN access (direct HTTP from local network)? (y/N): ");
45
45
  const lanEnabled = lanAnswer.trim().toLowerCase() === "y";
46
46
 
47
- let httpPort = 7400;
47
+ let httpPort = 9966;
48
48
  const portLabel = lanEnabled ? "HTTP port for local and LAN access" : "HTTP port for local access";
49
49
  const portAnswer = await ask(`${portLabel} (default ${httpPort}): `);
50
50
  const parsed = parseInt(portAnswer.trim(), 10);
@@ -2,7 +2,7 @@ import * as http from "node:http";
2
2
  import { StringCodec } from "nats";
3
3
  import { loadConfig } from "../config.js";
4
4
  import { connectNats } from "../nats-client.js";
5
- import { addSession } from "../session-store.js";
5
+ import { addClient } from "../client-store.js";
6
6
  import type { HostConfig } from "../types.js";
7
7
 
8
8
  const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
@@ -17,10 +17,10 @@ export function generatePairingCode(): string {
17
17
  }
18
18
 
19
19
  function buildPairResponse(config: HostConfig, label?: string) {
20
- const session = addSession(label);
20
+ const client = addClient(label);
21
21
  return {
22
22
  hostId: config.hostId,
23
- sessionToken: session.token,
23
+ clientToken: client.token,
24
24
  };
25
25
  }
26
26
 
@@ -67,7 +67,7 @@ function httpPairRegister(port: number, code: string): Promise<boolean> {
67
67
  export async function pairCommand(): Promise<void> {
68
68
  const config = loadConfig();
69
69
  const code = generatePairingCode();
70
- const httpPort = config.httpPort ?? 7400;
70
+ const httpPort = config.httpPort ?? 9966;
71
71
 
72
72
  let paired = false;
73
73
 
@@ -62,10 +62,15 @@ async function invokeAgentWithRetries(
62
62
  }, 500);
63
63
  }
64
64
 
65
- const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.transientPermissions);
65
+ const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(
66
+ invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions,
67
+ );
68
+ const truncate = (s: string, max = 100) => s.length > max ? s.slice(0, max) + "…" : s;
69
+ const displayArgs = args.map((a) => truncate(a));
70
+ console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
66
71
  const result = await spawnCommand(command, args, {
67
72
  cwd: getRunDir(ctx.taskDir, ctx.runId),
68
- 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) },
73
+ 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 ?? 9966) },
69
74
  echoStdout: true,
70
75
  resolveOnFailure: true,
71
76
  stdin,
@@ -304,7 +309,7 @@ async function runCommandTriggeredMode(
304
309
 
305
310
  const child = spawnStreamingCommand(commandStr, {
306
311
  cwd: getRunDir(ctx.taskDir, ctx.runId),
307
- 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) },
312
+ 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 ?? 9966) },
308
313
  });
309
314
 
310
315
  let linesProcessed = 0;
@@ -444,7 +449,7 @@ async function requestPermission(
444
449
  taskDir: string,
445
450
  requiredPermissions: RequiredPermission[],
446
451
  ): Promise<"granted" | "granted_all" | "aborted"> {
447
- const port = config.httpPort ?? 7400;
452
+ const port = config.httpPort ?? 9966;
448
453
  const res = await fetch(`http://localhost:${port}/request-permission`, {
449
454
  method: "POST",
450
455
  headers: { "Content-Type": "application/json" },
@@ -472,7 +477,7 @@ async function requestConfirmation(
472
477
  task: ParsedTask,
473
478
  taskDir: string,
474
479
  ): Promise<boolean> {
475
- const port = config.httpPort ?? 7400;
480
+ const port = config.httpPort ?? 9966;
476
481
  const res = await fetch(`http://localhost:${port}/request-confirmation`, {
477
482
  method: "POST",
478
483
  headers: { "Content-Type": "application/json" },
@@ -500,7 +505,8 @@ export function parseReportFiles(output: string): string[] {
500
505
  let match;
501
506
  while ((match = regex.exec(output)) !== null) {
502
507
  const name = match[1].trim();
503
- if (name) files.push(name);
508
+ // Skip placeholder examples echoed from the prompt (e.g. "<filename>")
509
+ if (name && !name.startsWith("<")) files.push(name);
504
510
  }
505
511
  return files;
506
512
  }
@@ -515,6 +521,8 @@ export function parsePermissions(output: string): RequiredPermission[] {
515
521
  let match;
516
522
  while ((match = regex.exec(output)) !== null) {
517
523
  const raw = match[1].trim();
524
+ // Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>")
525
+ if (raw.startsWith("<")) continue;
518
526
  const sep = raw.indexOf("|");
519
527
  if (sep !== -1) {
520
528
  perms.push({ name: raw.slice(0, sep).trim(), description: raw.slice(sep + 1).trim() });
@@ -531,8 +539,14 @@ export function parsePermissions(output: string): RequiredPermission[] {
531
539
  */
532
540
  export function parseTaskOutcome(output: string): TaskRunningState {
533
541
  const lastChunk = output.slice(-500);
534
- if (lastChunk.includes(TASK_FAILURE_MARKER)) return "failed";
535
- if (lastChunk.includes(TASK_SUCCESS_MARKER)) return "finished";
542
+ const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
543
+ let last: string | null = null;
544
+ let match;
545
+ while ((match = regex.exec(lastChunk)) !== null) {
546
+ last = match[0];
547
+ }
548
+ if (last === TASK_FAILURE_MARKER) return "failed";
549
+ if (last === TASK_SUCCESS_MARKER) return "finished";
536
550
  return "finished";
537
551
  }
538
552
 
@@ -114,7 +114,7 @@ export async function serveCommand(): Promise<void> {
114
114
  }, POLL_INTERVAL_MS);
115
115
 
116
116
  const handleRpc = createRpcHandler(config, nc);
117
- const httpPort = config.httpPort ?? 7400;
117
+ const httpPort = config.httpPort ?? 9966;
118
118
 
119
119
  // Start NATS transport (loops forever, fire-and-forget)
120
120
  if (nc) {
package/src/events.ts CHANGED
@@ -23,7 +23,7 @@ export async function publishHostEvent(
23
23
  }
24
24
 
25
25
  const config = loadConfig();
26
- const port = config.httpPort ?? 7400;
26
+ const port = config.httpPort ?? 9966;
27
27
  try {
28
28
  await fetch(`http://localhost:${port}/event`, {
29
29
  method: "POST",
package/src/index.ts CHANGED
@@ -12,7 +12,7 @@ import { serveCommand } from "./commands/serve.js";
12
12
 
13
13
  import { pairCommand } from "./commands/pair.js";
14
14
  import { restartCommand } from "./commands/restart.js";
15
- import { sessionsListCommand, sessionsRevokeCommand, sessionsRevokeAllCommand } from "./commands/sessions.js";
15
+ import { clientsListCommand, clientsRevokeCommand, clientsRevokeAllCommand } from "./commands/clients.js";
16
16
 
17
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
18
  const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
@@ -68,29 +68,29 @@ program
68
68
  });
69
69
 
70
70
 
71
- const sessionsCmd = program
72
- .command("sessions")
73
- .description("Manage paired client sessions");
71
+ const clientsCmd = program
72
+ .command("clients")
73
+ .description("Manage paired clients");
74
74
 
75
- sessionsCmd
75
+ clientsCmd
76
76
  .command("list")
77
- .description("List active sessions")
77
+ .description("List active clients")
78
78
  .action(async () => {
79
- await sessionsListCommand();
79
+ await clientsListCommand();
80
80
  });
81
81
 
82
- sessionsCmd
82
+ clientsCmd
83
83
  .command("revoke <token>")
84
- .description("Revoke a session by token")
84
+ .description("Revoke a client by token")
85
85
  .action(async (token: string) => {
86
- await sessionsRevokeCommand(token);
86
+ await clientsRevokeCommand(token);
87
87
  });
88
88
 
89
- sessionsCmd
89
+ clientsCmd
90
90
  .command("revoke-all")
91
- .description("Revoke all sessions")
91
+ .description("Revoke all clients")
92
92
  .action(async () => {
93
- await sessionsRevokeAllCommand();
93
+ await clientsRevokeAllCommand();
94
94
  });
95
95
 
96
96
  // No subcommand → default to serve
@@ -11,7 +11,7 @@ import { getPlatform } from "./platform/index.js";
11
11
  import { spawnCommand } from "./spawn-command.js";
12
12
  import crossSpawn from "cross-spawn";
13
13
  import { getAgent } from "./agents/agent.js";
14
- import { validateSession } from "./session-store.js";
14
+ import { validateClient } from "./client-store.js";
15
15
  import { publishHostEvent } from "./events.js";
16
16
  import { currentVersion, performUpdate } from "./update-checker.js";
17
17
  import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
@@ -166,8 +166,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
166
166
  }
167
167
 
168
168
  async function handleRpc(request: RpcMessage): Promise<unknown> {
169
- // Session token validation: skip for trusted localhost requests
170
- if (!request.localhost && (!request.sessionToken || !validateSession(request.sessionToken))) {
169
+ // Client token validation: skip for trusted localhost requests
170
+ if (!request.localhost && (!request.clientToken || !validateClient(request.clientToken))) {
171
171
  return { error: "Unauthorized" };
172
172
  }
173
173
 
@@ -188,6 +188,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
188
188
  triggers?: Array<{ type: "cron" | "once"; value: string }>;
189
189
  triggers_enabled?: boolean;
190
190
  requires_confirmation?: boolean;
191
+ yolo_mode?: boolean;
191
192
  command?: string;
192
193
  };
193
194
 
@@ -218,6 +219,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
218
219
  triggers: params.triggers ?? [],
219
220
  triggers_enabled: params.triggers_enabled ?? true,
220
221
  requires_confirmation: params.requires_confirmation ?? true,
222
+ ...(params.yolo_mode ? { yolo_mode: true } : {}),
221
223
  ...(params.command ? { command: params.command } : {}),
222
224
  },
223
225
  body,
@@ -238,6 +240,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
238
240
  triggers?: Array<{ type: "cron" | "once"; value: string }>;
239
241
  triggers_enabled?: boolean;
240
242
  requires_confirmation?: boolean;
243
+ yolo_mode?: boolean;
241
244
  command?: string;
242
245
  };
243
246
 
@@ -256,6 +259,10 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
256
259
  if (params.triggers_enabled !== undefined) existing.frontmatter.triggers_enabled = params.triggers_enabled;
257
260
  if (params.requires_confirmation !== undefined)
258
261
  existing.frontmatter.requires_confirmation = params.requires_confirmation;
262
+ if (params.yolo_mode !== undefined) {
263
+ existing.frontmatter.yolo_mode = params.yolo_mode || undefined;
264
+ if (params.yolo_mode) delete existing.frontmatter.permissions;
265
+ }
259
266
  if (params.command !== undefined) {
260
267
  if (params.command) {
261
268
  existing.frontmatter.command = params.command;
@@ -302,6 +309,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
302
309
  user_prompt: string;
303
310
  agent: string;
304
311
  requires_confirmation?: boolean;
312
+ yolo_mode?: boolean;
305
313
  command?: string;
306
314
  };
307
315
 
@@ -317,6 +325,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
317
325
  triggers: [],
318
326
  triggers_enabled: false,
319
327
  requires_confirmation: params.requires_confirmation ?? false,
328
+ ...(params.yolo_mode ? { yolo_mode: true } : {}),
320
329
  ...(params.command ? { command: params.command } : {}),
321
330
  },
322
331
  body: "",
@@ -390,7 +399,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
390
399
  // Fire-and-forget: invoke agent inline as a child of the serve process
391
400
  const followupAgent = getAgent(followupTask.frontmatter.agent);
392
401
  const { command: cmd, args: cmdArgs, stdin } = followupAgent.getTaskRunCommandLine(
393
- followupTask, params.message, followupTask.frontmatter.permissions,
402
+ followupTask, params.message, followupTask.frontmatter.yolo_mode ? "yolo" : followupTask.frontmatter.permissions,
394
403
  );
395
404
 
396
405
  // Spawn directly via crossSpawn so we can track and kill the child
@@ -564,8 +573,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
564
573
  const reports: Array<{ file: string; content?: string; error?: string }> = [];
565
574
  const runDir = path.join(config.projectRoot, "tasks", params.id, params.run_id);
566
575
  for (const file of params.report_files) {
567
- if (!file.endsWith(".md")) {
568
- reports.push({ file, error: "must end with .md" });
576
+ if (!file.endsWith(".md") && !file.endsWith(".txt")) {
577
+ reports.push({ file, error: "must end with .md or .txt" });
569
578
  continue;
570
579
  }
571
580
  const basename = path.basename(file);