palmier 0.5.2 → 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 (42) hide show
  1. package/README.md +9 -9
  2. package/dist/agents/agent-instructions.md +7 -7
  3. package/dist/agents/shared-prompt.js +1 -1
  4. package/dist/client-store.d.ts +12 -0
  5. package/dist/client-store.js +57 -0
  6. package/dist/commands/clients.d.ts +4 -0
  7. package/dist/commands/clients.js +27 -0
  8. package/dist/commands/info.js +5 -5
  9. package/dist/commands/init.js +1 -1
  10. package/dist/commands/pair.js +4 -4
  11. package/dist/commands/run.js +9 -5
  12. package/dist/commands/serve.js +1 -1
  13. package/dist/events.js +1 -1
  14. package/dist/index.js +13 -13
  15. package/dist/rpc-handler.js +7 -4
  16. package/dist/transports/http-transport.js +7 -7
  17. package/dist/transports/nats-transport.js +4 -4
  18. package/dist/types.d.ts +2 -2
  19. package/package.json +1 -1
  20. package/src/agents/agent-instructions.md +7 -7
  21. package/src/agents/shared-prompt.ts +1 -1
  22. package/src/client-store.ts +68 -0
  23. package/src/commands/clients.ts +29 -0
  24. package/src/commands/info.ts +5 -5
  25. package/src/commands/init.ts +1 -1
  26. package/src/commands/pair.ts +4 -4
  27. package/src/commands/run.ts +8 -5
  28. package/src/commands/serve.ts +1 -1
  29. package/src/events.ts +1 -1
  30. package/src/index.ts +13 -13
  31. package/src/rpc-handler.ts +7 -4
  32. package/src/transports/http-transport.ts +7 -7
  33. package/src/transports/nats-transport.ts +4 -4
  34. package/src/types.ts +3 -3
  35. package/test/agent-instructions.test.ts +22 -5
  36. package/test/agent-output-parsing.test.ts +12 -0
  37. package/dist/commands/sessions.d.ts +0 -4
  38. package/dist/commands/sessions.js +0 -27
  39. package/dist/session-store.d.ts +0 -12
  40. package/dist/session-store.js +0 -57
  41. package/src/commands/sessions.ts +0 -29
  42. package/src/session-store.ts +0 -68
package/README.md CHANGED
@@ -46,9 +46,9 @@ All `palmier` commands should be run from a dedicated Palmier root directory (e.
46
46
  |---|---|
47
47
  | `palmier init` | Interactive setup wizard |
48
48
  | `palmier pair` | Generate an OTP code to pair a new device |
49
- | `palmier sessions list` | List active session tokens |
50
- | `palmier sessions revoke <token>` | Revoke a specific session token |
51
- | `palmier sessions revoke-all` | Revoke all session tokens |
49
+ | `palmier clients list` | List active client tokens |
50
+ | `palmier clients revoke <token>` | Revoke a specific client token |
51
+ | `palmier clients revoke-all` | Revoke all client tokens |
52
52
  | `palmier info` | Show host connection info (address, mode) |
53
53
  | `palmier serve` | Run the persistent RPC handler (default command) |
54
54
  | `palmier restart` | Restart the palmier serve daemon |
@@ -70,17 +70,17 @@ Local access (`http://localhost:<port>`) works immediately — no pairing needed
70
70
 
71
71
  For LAN or server mode, run `palmier pair` on the host to generate an OTP code. Enter it in the PWA — either at `http://<host-ip>:<port>` (LAN mode) or `https://app.palmier.me` (server mode).
72
72
 
73
- ### Managing sessions
73
+ ### Managing clients
74
74
 
75
75
  ```bash
76
76
  # List all paired devices
77
- palmier sessions list
77
+ palmier clients list
78
78
 
79
79
  # Revoke a specific device's access
80
- palmier sessions revoke <token>
80
+ palmier clients revoke <token>
81
81
 
82
- # Revoke all sessions (unpair all devices)
83
- palmier sessions revoke-all
82
+ # Revoke all clients (unpair all devices)
83
+ palmier clients revoke-all
84
84
  ```
85
85
 
86
86
  The `init` command:
@@ -126,7 +126,7 @@ palmier restart
126
126
  ## How It Works
127
127
 
128
128
  - The host runs as a **background daemon** (systemd user service on Linux, Registry Run key on Windows), staying alive via `palmier serve`.
129
- - **Device access** — localhost is always trusted (no pairing needed). LAN and server mode devices communicate via direct HTTP or NATS respectively, and must pair via OTP to get a session token.
129
+ - **Device access** — localhost is always trusted (no pairing needed). LAN and server mode devices communicate via direct HTTP or NATS respectively, and must pair via OTP to get a client token.
130
130
  - **Tasks** are stored locally as Markdown files in a `tasks/` directory. Each task has a name, prompt, execution plan, and optional schedules (cron schedules or one-time dates).
131
131
  - **Plan generation** is automatic — when you create or update a task, the host invokes your chosen agent CLI to generate an execution plan and name.
132
132
  - **Schedules** are backed by systemd timers (Linux) or Task Scheduler (Windows). You can enable/disable them without deleting the task, and any task can still be run manually at any time.
@@ -2,19 +2,19 @@ You are an AI agent executing a task on behalf of the user via the Palmier platf
2
2
 
3
3
  ## Reporting Output
4
4
 
5
- If you generate report or output files, print each file path on its own line using this exact format (no code block):
6
- `[PALMIER_REPORT] <filename>`
5
+ If you generate report or output files, print each file path on its own line using this exact format:
6
+ [PALMIER_REPORT] <filename>
7
7
 
8
8
  ## Completion
9
9
 
10
- When you are done, output exactly one of these markers as the very last line (no code block, no other text on the same line):
11
- - Success: `[PALMIER_TASK_SUCCESS]`
12
- - Failure: `[PALMIER_TASK_FAILURE]`
10
+ When you are done, output exactly one of these markers as the very last line (no other text on the same line):
11
+ [PALMIER_TASK_SUCCESS]
12
+ [PALMIER_TASK_FAILURE]
13
13
 
14
14
  ## Permissions
15
15
 
16
- If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line using this exact format (no code block):
17
- `[PALMIER_PERMISSION] <tool_name> | <description>`
16
+ If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line using this exact format:
17
+ [PALMIER_PERMISSION] <tool_name> | <description>
18
18
 
19
19
  ## HTTP Endpoints
20
20
 
@@ -8,7 +8,7 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(path.join(__dirname, "agent-
8
8
  * Agent instructions with the serve daemon's HTTP port and task ID baked in.
9
9
  */
10
10
  export function getAgentInstructions(taskId, skipPermissions) {
11
- const port = loadConfig().httpPort ?? 7400;
11
+ const port = loadConfig().httpPort ?? 9966;
12
12
  let instructions = AGENT_INSTRUCTIONS_TEMPLATE
13
13
  .replace(/\{\{PORT\}\}/g, String(port))
14
14
  .replace(/\{\{TASK_ID\}\}/g, taskId);
@@ -0,0 +1,12 @@
1
+ export interface ClientEntry {
2
+ token: string;
3
+ createdAt: string;
4
+ label?: string;
5
+ }
6
+ export declare function loadClients(): ClientEntry[];
7
+ export declare function addClient(label?: string): ClientEntry;
8
+ export declare function revokeClient(token: string): boolean;
9
+ export declare function revokeAllClients(): number;
10
+ export declare function validateClient(token: string): boolean;
11
+ export declare function hasClients(): boolean;
12
+ //# sourceMappingURL=client-store.d.ts.map
@@ -0,0 +1,57 @@
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
+ const CLIENTS_FILE = path.join(CONFIG_DIR, "clients.json");
6
+ function readFile() {
7
+ try {
8
+ if (!fs.existsSync(CLIENTS_FILE))
9
+ return [];
10
+ const raw = fs.readFileSync(CLIENTS_FILE, "utf-8");
11
+ return JSON.parse(raw);
12
+ }
13
+ catch {
14
+ return [];
15
+ }
16
+ }
17
+ function writeFile(clients) {
18
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
19
+ fs.writeFileSync(CLIENTS_FILE, JSON.stringify(clients, null, 2), "utf-8");
20
+ }
21
+ export function loadClients() {
22
+ return readFile();
23
+ }
24
+ export function addClient(label) {
25
+ const clients = readFile();
26
+ const entry = {
27
+ token: randomBytes(32).toString("hex"),
28
+ createdAt: new Date().toISOString(),
29
+ ...(label ? { label } : {}),
30
+ };
31
+ clients.push(entry);
32
+ writeFile(clients);
33
+ return entry;
34
+ }
35
+ export function revokeClient(token) {
36
+ const clients = readFile();
37
+ const idx = clients.findIndex((c) => c.token === token);
38
+ if (idx === -1)
39
+ return false;
40
+ clients.splice(idx, 1);
41
+ writeFile(clients);
42
+ return true;
43
+ }
44
+ export function revokeAllClients() {
45
+ const clients = readFile();
46
+ const count = clients.length;
47
+ writeFile([]);
48
+ return count;
49
+ }
50
+ export function validateClient(token) {
51
+ const clients = readFile();
52
+ return clients.some((c) => c.token === token);
53
+ }
54
+ export function hasClients() {
55
+ return readFile().length > 0;
56
+ }
57
+ //# sourceMappingURL=client-store.js.map
@@ -0,0 +1,4 @@
1
+ export declare function clientsListCommand(): Promise<void>;
2
+ export declare function clientsRevokeCommand(token: string): Promise<void>;
3
+ export declare function clientsRevokeAllCommand(): Promise<void>;
4
+ //# sourceMappingURL=clients.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { loadClients, revokeClient, revokeAllClients } from "../client-store.js";
2
+ export async function clientsListCommand() {
3
+ const clients = loadClients();
4
+ if (clients.length === 0) {
5
+ console.log("No active clients.");
6
+ return;
7
+ }
8
+ console.log(`${clients.length} active client(s):\n`);
9
+ for (const c of clients) {
10
+ const label = c.label ? ` (${c.label})` : "";
11
+ console.log(` ${c.token}${label} created ${c.createdAt}`);
12
+ }
13
+ }
14
+ export async function clientsRevokeCommand(token) {
15
+ if (revokeClient(token)) {
16
+ console.log("Client revoked.");
17
+ }
18
+ else {
19
+ console.error("Client not found.");
20
+ process.exit(1);
21
+ }
22
+ }
23
+ export async function clientsRevokeAllCommand() {
24
+ const count = revokeAllClients();
25
+ console.log(`Revoked ${count} client(s).`);
26
+ }
27
+ //# sourceMappingURL=clients.js.map
@@ -1,11 +1,11 @@
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
  * Print host connection info for setting up clients.
5
5
  */
6
6
  export async function infoCommand() {
7
7
  const config = loadConfig();
8
- const sessions = loadSessions();
8
+ const clients = loadClients();
9
9
  console.log(`Host ID: ${config.hostId}`);
10
10
  console.log(`Project root: ${config.projectRoot}`);
11
11
  // Detected agents
@@ -15,9 +15,9 @@ export async function infoCommand() {
15
15
  else {
16
16
  console.log(`Agents: (none detected — run \`palmier agents\`)`);
17
17
  }
18
- // Sessions
19
- console.log(`Sessions: ${sessions.length} active`);
20
- if (sessions.length === 0) {
18
+ // Clients
19
+ console.log(`Clients: ${clients.length} active`);
20
+ if (clients.length === 0) {
21
21
  console.log("");
22
22
  console.log("No paired clients. Run `palmier pair` to connect a device.");
23
23
  }
@@ -33,7 +33,7 @@ export async function initCommand() {
33
33
  // LAN mode
34
34
  const lanAnswer = await ask("Enable LAN access (direct HTTP from local network)? (y/N): ");
35
35
  const lanEnabled = lanAnswer.trim().toLowerCase() === "y";
36
- let httpPort = 7400;
36
+ let httpPort = 9966;
37
37
  const portLabel = lanEnabled ? "HTTP port for local and LAN access" : "HTTP port for local access";
38
38
  const portAnswer = await ask(`${portLabel} (default ${httpPort}): `);
39
39
  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
  const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
7
7
  const CODE_LENGTH = 6;
8
8
  export const PAIRING_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
@@ -12,10 +12,10 @@ export function generatePairingCode() {
12
12
  return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
13
13
  }
14
14
  function buildPairResponse(config, label) {
15
- const session = addSession(label);
15
+ const client = addClient(label);
16
16
  return {
17
17
  hostId: config.hostId,
18
- sessionToken: session.token,
18
+ clientToken: client.token,
19
19
  };
20
20
  }
21
21
  /**
@@ -56,7 +56,7 @@ function httpPairRegister(port, code) {
56
56
  export async function pairCommand() {
57
57
  const config = loadConfig();
58
58
  const code = generatePairingCode();
59
- const httpPort = config.httpPort ?? 7400;
59
+ const httpPort = config.httpPort ?? 9966;
60
60
  let paired = false;
61
61
  function onPaired() {
62
62
  paired = true;
@@ -39,7 +39,7 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
39
39
  console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
40
40
  const result = await spawnCommand(command, args, {
41
41
  cwd: getRunDir(ctx.taskDir, ctx.runId),
42
- 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) },
42
+ 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) },
43
43
  echoStdout: true,
44
44
  resolveOnFailure: true,
45
45
  stdin,
@@ -248,7 +248,7 @@ async function runCommandTriggeredMode(ctx) {
248
248
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
249
249
  const child = spawnStreamingCommand(commandStr, {
250
250
  cwd: getRunDir(ctx.taskDir, ctx.runId),
251
- 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) },
251
+ 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) },
252
252
  });
253
253
  let linesProcessed = 0;
254
254
  let invocationsSucceeded = 0;
@@ -365,7 +365,7 @@ async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName
365
365
  await publishHostEvent(nc, config.hostId, taskId, payload);
366
366
  }
367
367
  async function requestPermission(config, task, taskDir, requiredPermissions) {
368
- const port = config.httpPort ?? 7400;
368
+ const port = config.httpPort ?? 9966;
369
369
  const res = await fetch(`http://localhost:${port}/request-permission`, {
370
370
  method: "POST",
371
371
  headers: { "Content-Type": "application/json" },
@@ -387,7 +387,7 @@ async function requestPermission(config, task, taskDir, requiredPermissions) {
387
387
  return response;
388
388
  }
389
389
  async function requestConfirmation(config, task, taskDir) {
390
- const port = config.httpPort ?? 7400;
390
+ const port = config.httpPort ?? 9966;
391
391
  const res = await fetch(`http://localhost:${port}/request-confirmation`, {
392
392
  method: "POST",
393
393
  headers: { "Content-Type": "application/json" },
@@ -414,7 +414,8 @@ export function parseReportFiles(output) {
414
414
  let match;
415
415
  while ((match = regex.exec(output)) !== null) {
416
416
  const name = match[1].trim();
417
- if (name)
417
+ // Skip placeholder examples echoed from the prompt (e.g. "<filename>")
418
+ if (name && !name.startsWith("<"))
418
419
  files.push(name);
419
420
  }
420
421
  return files;
@@ -429,6 +430,9 @@ export function parsePermissions(output) {
429
430
  let match;
430
431
  while ((match = regex.exec(output)) !== null) {
431
432
  const raw = match[1].trim();
433
+ // Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>")
434
+ if (raw.startsWith("<"))
435
+ continue;
432
436
  const sep = raw.indexOf("|");
433
437
  if (sep !== -1) {
434
438
  perms.push({ name: raw.slice(0, sep).trim(), description: raw.slice(sep + 1).trim() });
@@ -99,7 +99,7 @@ export async function serveCommand() {
99
99
  });
100
100
  }, POLL_INTERVAL_MS);
101
101
  const handleRpc = createRpcHandler(config, nc);
102
- const httpPort = config.httpPort ?? 7400;
102
+ const httpPort = config.httpPort ?? 9966;
103
103
  // Start NATS transport (loops forever, fire-and-forget)
104
104
  if (nc) {
105
105
  startNatsTransport(config, handleRpc, nc);
package/dist/events.js CHANGED
@@ -14,7 +14,7 @@ export async function publishHostEvent(nc, hostId, taskId, payload) {
14
14
  console.log(`[nats] ${subject} →`, payload);
15
15
  }
16
16
  const config = loadConfig();
17
- const port = config.httpPort ?? 7400;
17
+ const port = config.httpPort ?? 9966;
18
18
  try {
19
19
  await fetch(`http://localhost:${port}/event`, {
20
20
  method: "POST",
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { runCommand } from "./commands/run.js";
10
10
  import { serveCommand } from "./commands/serve.js";
11
11
  import { pairCommand } from "./commands/pair.js";
12
12
  import { restartCommand } from "./commands/restart.js";
13
- import { sessionsListCommand, sessionsRevokeCommand, sessionsRevokeAllCommand } from "./commands/sessions.js";
13
+ import { clientsListCommand, clientsRevokeCommand, clientsRevokeAllCommand } from "./commands/clients.js";
14
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
15
  const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
16
16
  const program = new Command();
@@ -54,26 +54,26 @@ program
54
54
  .action(async () => {
55
55
  await pairCommand();
56
56
  });
57
- const sessionsCmd = program
58
- .command("sessions")
59
- .description("Manage paired client sessions");
60
- sessionsCmd
57
+ const clientsCmd = program
58
+ .command("clients")
59
+ .description("Manage paired clients");
60
+ clientsCmd
61
61
  .command("list")
62
- .description("List active sessions")
62
+ .description("List active clients")
63
63
  .action(async () => {
64
- await sessionsListCommand();
64
+ await clientsListCommand();
65
65
  });
66
- sessionsCmd
66
+ clientsCmd
67
67
  .command("revoke <token>")
68
- .description("Revoke a session by token")
68
+ .description("Revoke a client by token")
69
69
  .action(async (token) => {
70
- await sessionsRevokeCommand(token);
70
+ await clientsRevokeCommand(token);
71
71
  });
72
- sessionsCmd
72
+ clientsCmd
73
73
  .command("revoke-all")
74
- .description("Revoke all sessions")
74
+ .description("Revoke all clients")
75
75
  .action(async () => {
76
- await sessionsRevokeAllCommand();
76
+ await clientsRevokeAllCommand();
77
77
  });
78
78
  // No subcommand → default to serve
79
79
  if (process.argv.length <= 2) {
@@ -10,7 +10,7 @@ import { getPlatform } from "./platform/index.js";
10
10
  import { spawnCommand } from "./spawn-command.js";
11
11
  import crossSpawn from "cross-spawn";
12
12
  import { getAgent } from "./agents/agent.js";
13
- import { validateSession } from "./session-store.js";
13
+ import { validateClient } from "./client-store.js";
14
14
  import { publishHostEvent } from "./events.js";
15
15
  import { currentVersion, performUpdate } from "./update-checker.js";
16
16
  import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
@@ -140,8 +140,8 @@ export function createRpcHandler(config, nc) {
140
140
  };
141
141
  }
142
142
  async function handleRpc(request) {
143
- // Session token validation: skip for trusted localhost requests
144
- if (!request.localhost && (!request.sessionToken || !validateSession(request.sessionToken))) {
143
+ // Client token validation: skip for trusted localhost requests
144
+ if (!request.localhost && (!request.clientToken || !validateClient(request.clientToken))) {
145
145
  return { error: "Unauthorized" };
146
146
  }
147
147
  switch (request.method) {
@@ -212,8 +212,11 @@ export function createRpcHandler(config, nc) {
212
212
  existing.frontmatter.triggers_enabled = params.triggers_enabled;
213
213
  if (params.requires_confirmation !== undefined)
214
214
  existing.frontmatter.requires_confirmation = params.requires_confirmation;
215
- if (params.yolo_mode !== undefined)
215
+ if (params.yolo_mode !== undefined) {
216
216
  existing.frontmatter.yolo_mode = params.yolo_mode || undefined;
217
+ if (params.yolo_mode)
218
+ delete existing.frontmatter.permissions;
219
+ }
217
220
  if (params.command !== undefined) {
218
221
  if (params.command) {
219
222
  existing.frontmatter.command = params.command;
@@ -1,7 +1,7 @@
1
1
  import * as http from "node:http";
2
2
  import * as os from "os";
3
3
  import { StringCodec } from "nats";
4
- import { validateSession, addSession } from "../session-store.js";
4
+ import { validateClient, addClient } from "../client-store.js";
5
5
  import { registerPending } from "../pending-requests.js";
6
6
  import * as fs from "node:fs";
7
7
  import { getTaskDir, parseTaskFile, spliceUserMessage } from "../task.js";
@@ -115,9 +115,9 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
115
115
  const auth = req.headers.authorization;
116
116
  if (!auth || !auth.startsWith("Bearer "))
117
117
  return false;
118
- return validateSession(auth.slice(7));
118
+ return validateClient(auth.slice(7));
119
119
  }
120
- function extractSessionToken(req) {
120
+ function extractClientToken(req) {
121
121
  const auth = req.headers.authorization;
122
122
  if (!auth || !auth.startsWith("Bearer "))
123
123
  return undefined;
@@ -368,11 +368,11 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
368
368
  sendJson(res, 401, { error: "Invalid code" });
369
369
  return;
370
370
  }
371
- const session = addSession(label);
371
+ const client = addClient(label);
372
372
  const ip = detectLanIp();
373
373
  const response = {
374
374
  hostId: config.hostId,
375
- sessionToken: session.token,
375
+ clientToken: client.token,
376
376
  directUrl: `http://${ip}:${port}`,
377
377
  };
378
378
  clearTimeout(pending.timer);
@@ -449,10 +449,10 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
449
449
  sendJson(res, 400, { error: "Invalid JSON" });
450
450
  return;
451
451
  }
452
- const sessionToken = extractSessionToken(req);
452
+ const clientToken = extractClientToken(req);
453
453
  console.log(`[http] RPC: ${method}`);
454
454
  try {
455
- const response = await handleRpc({ method, params, sessionToken, localhost: isLocalhost(req) });
455
+ const response = await handleRpc({ method, params, clientToken, localhost: isLocalhost(req) });
456
456
  console.log(`[http] RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
457
457
  sendJson(res, 200, response);
458
458
  }
@@ -39,13 +39,13 @@ export async function startNatsTransport(config, handleRpc, nc) {
39
39
  }
40
40
  }
41
41
  }
42
- // Extract sessionToken from params (PWA includes it in the payload)
43
- const sessionToken = typeof params.sessionToken === "string" ? params.sessionToken : undefined;
44
- delete params.sessionToken;
42
+ // Extract clientToken from params (PWA includes it in the payload)
43
+ const clientToken = typeof params.clientToken === "string" ? params.clientToken : undefined;
44
+ delete params.clientToken;
45
45
  console.log(`[nats] RPC: ${method}`);
46
46
  let response;
47
47
  try {
48
- response = await handleRpc({ method, params, sessionToken });
48
+ response = await handleRpc({ method, params, clientToken });
49
49
  }
50
50
  catch (err) {
51
51
  console.error(`[nats] RPC error (${method}):`, err);
package/dist/types.d.ts CHANGED
@@ -67,8 +67,8 @@ export interface ConversationMessage {
67
67
  export interface RpcMessage {
68
68
  method: string;
69
69
  params: Record<string, unknown>;
70
- sessionToken?: string;
71
- /** Trusted localhost request — skip session validation. */
70
+ clientToken?: string;
71
+ /** Trusted localhost request — skip client validation. */
72
72
  localhost?: boolean;
73
73
  }
74
74
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -2,19 +2,19 @@ You are an AI agent executing a task on behalf of the user via the Palmier platf
2
2
 
3
3
  ## Reporting Output
4
4
 
5
- If you generate report or output files, print each file path on its own line using this exact format (no code block):
6
- `[PALMIER_REPORT] <filename>`
5
+ If you generate report or output files, print each file path on its own line using this exact format:
6
+ [PALMIER_REPORT] <filename>
7
7
 
8
8
  ## Completion
9
9
 
10
- When you are done, output exactly one of these markers as the very last line (no code block, no other text on the same line):
11
- - Success: `[PALMIER_TASK_SUCCESS]`
12
- - Failure: `[PALMIER_TASK_FAILURE]`
10
+ When you are done, output exactly one of these markers as the very last line (no other text on the same line):
11
+ [PALMIER_TASK_SUCCESS]
12
+ [PALMIER_TASK_FAILURE]
13
13
 
14
14
  ## Permissions
15
15
 
16
- If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line using this exact format (no code block):
17
- `[PALMIER_PERMISSION] <tool_name> | <description>`
16
+ If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line using this exact format:
17
+ [PALMIER_PERMISSION] <tool_name> | <description>
18
18
 
19
19
  ## HTTP Endpoints
20
20
 
@@ -14,7 +14,7 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
14
14
  * Agent instructions with the serve daemon's HTTP port and task ID baked in.
15
15
  */
16
16
  export function getAgentInstructions(taskId: string, skipPermissions?: boolean): string {
17
- const port = loadConfig().httpPort ?? 7400;
17
+ const port = loadConfig().httpPort ?? 9966;
18
18
  let instructions = AGENT_INSTRUCTIONS_TEMPLATE
19
19
  .replace(/\{\{PORT\}\}/g, String(port))
20
20
  .replace(/\{\{TASK_ID\}\}/g, taskId);
@@ -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
 
@@ -70,7 +70,7 @@ async function invokeAgentWithRetries(
70
70
  console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
71
71
  const result = await spawnCommand(command, args, {
72
72
  cwd: getRunDir(ctx.taskDir, ctx.runId),
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 ?? 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) },
74
74
  echoStdout: true,
75
75
  resolveOnFailure: true,
76
76
  stdin,
@@ -309,7 +309,7 @@ async function runCommandTriggeredMode(
309
309
 
310
310
  const child = spawnStreamingCommand(commandStr, {
311
311
  cwd: getRunDir(ctx.taskDir, ctx.runId),
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 ?? 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) },
313
313
  });
314
314
 
315
315
  let linesProcessed = 0;
@@ -449,7 +449,7 @@ async function requestPermission(
449
449
  taskDir: string,
450
450
  requiredPermissions: RequiredPermission[],
451
451
  ): Promise<"granted" | "granted_all" | "aborted"> {
452
- const port = config.httpPort ?? 7400;
452
+ const port = config.httpPort ?? 9966;
453
453
  const res = await fetch(`http://localhost:${port}/request-permission`, {
454
454
  method: "POST",
455
455
  headers: { "Content-Type": "application/json" },
@@ -477,7 +477,7 @@ async function requestConfirmation(
477
477
  task: ParsedTask,
478
478
  taskDir: string,
479
479
  ): Promise<boolean> {
480
- const port = config.httpPort ?? 7400;
480
+ const port = config.httpPort ?? 9966;
481
481
  const res = await fetch(`http://localhost:${port}/request-confirmation`, {
482
482
  method: "POST",
483
483
  headers: { "Content-Type": "application/json" },
@@ -505,7 +505,8 @@ export function parseReportFiles(output: string): string[] {
505
505
  let match;
506
506
  while ((match = regex.exec(output)) !== null) {
507
507
  const name = match[1].trim();
508
- if (name) files.push(name);
508
+ // Skip placeholder examples echoed from the prompt (e.g. "<filename>")
509
+ if (name && !name.startsWith("<")) files.push(name);
509
510
  }
510
511
  return files;
511
512
  }
@@ -520,6 +521,8 @@ export function parsePermissions(output: string): RequiredPermission[] {
520
521
  let match;
521
522
  while ((match = regex.exec(output)) !== null) {
522
523
  const raw = match[1].trim();
524
+ // Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>")
525
+ if (raw.startsWith("<")) continue;
523
526
  const sep = raw.indexOf("|");
524
527
  if (sep !== -1) {
525
528
  perms.push({ name: raw.slice(0, sep).trim(), description: raw.slice(sep + 1).trim() });
@@ -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
 
@@ -259,7 +259,10 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
259
259
  if (params.triggers_enabled !== undefined) existing.frontmatter.triggers_enabled = params.triggers_enabled;
260
260
  if (params.requires_confirmation !== undefined)
261
261
  existing.frontmatter.requires_confirmation = params.requires_confirmation;
262
- if (params.yolo_mode !== undefined) existing.frontmatter.yolo_mode = params.yolo_mode || undefined;
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
+ }
263
266
  if (params.command !== undefined) {
264
267
  if (params.command) {
265
268
  existing.frontmatter.command = params.command;
@@ -1,7 +1,7 @@
1
1
  import * as http from "node:http";
2
2
  import * as os from "os";
3
3
  import { StringCodec, type NatsConnection } from "nats";
4
- import { validateSession, addSession } from "../session-store.js";
4
+ import { validateClient, addClient } from "../client-store.js";
5
5
  import { registerPending } from "../pending-requests.js";
6
6
  import * as fs from "node:fs";
7
7
  import { getTaskDir, parseTaskFile, spliceUserMessage } from "../task.js";
@@ -145,10 +145,10 @@ export async function startHttpTransport(
145
145
  function checkAuth(req: http.IncomingMessage): boolean {
146
146
  const auth = req.headers.authorization;
147
147
  if (!auth || !auth.startsWith("Bearer ")) return false;
148
- return validateSession(auth.slice(7));
148
+ return validateClient(auth.slice(7));
149
149
  }
150
150
 
151
- function extractSessionToken(req: http.IncomingMessage): string | undefined {
151
+ function extractClientToken(req: http.IncomingMessage): string | undefined {
152
152
  const auth = req.headers.authorization;
153
153
  if (!auth || !auth.startsWith("Bearer ")) return undefined;
154
154
  return auth.slice(7);
@@ -394,11 +394,11 @@ export async function startHttpTransport(
394
394
  const pending = pendingPairs.get(code);
395
395
  if (!pending) { sendJson(res, 401, { error: "Invalid code" }); return; }
396
396
 
397
- const session = addSession(label);
397
+ const client = addClient(label);
398
398
  const ip = detectLanIp();
399
399
  const response: Record<string, unknown> = {
400
400
  hostId: config.hostId,
401
- sessionToken: session.token,
401
+ clientToken: client.token,
402
402
  directUrl: `http://${ip}:${port}`,
403
403
  };
404
404
 
@@ -476,11 +476,11 @@ export async function startHttpTransport(
476
476
  }
477
477
  } catch { sendJson(res, 400, { error: "Invalid JSON" }); return; }
478
478
 
479
- const sessionToken = extractSessionToken(req);
479
+ const clientToken = extractClientToken(req);
480
480
  console.log(`[http] RPC: ${method}`);
481
481
 
482
482
  try {
483
- const response = await handleRpc({ method, params, sessionToken, localhost: isLocalhost(req) });
483
+ const response = await handleRpc({ method, params, clientToken, localhost: isLocalhost(req) });
484
484
  console.log(`[http] RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
485
485
  sendJson(res, 200, response);
486
486
  } catch (err) {
@@ -50,15 +50,15 @@ export async function startNatsTransport(
50
50
  }
51
51
  }
52
52
 
53
- // Extract sessionToken from params (PWA includes it in the payload)
54
- const sessionToken = typeof params.sessionToken === "string" ? params.sessionToken : undefined;
55
- delete params.sessionToken;
53
+ // Extract clientToken from params (PWA includes it in the payload)
54
+ const clientToken = typeof params.clientToken === "string" ? params.clientToken : undefined;
55
+ delete params.clientToken;
56
56
 
57
57
  console.log(`[nats] RPC: ${method}`);
58
58
 
59
59
  let response: unknown;
60
60
  try {
61
- response = await handleRpc({ method, params, sessionToken });
61
+ response = await handleRpc({ method, params, clientToken });
62
62
  } catch (err) {
63
63
  console.error(`[nats] RPC error (${method}):`, err);
64
64
  response = { error: String(err) };
package/src/types.ts CHANGED
@@ -9,7 +9,7 @@ export interface HostConfig {
9
9
  // Detected agent CLIs
10
10
  agents?: Array<{ key: string; label: string }>;
11
11
 
12
- // HTTP server port (default 7400)
12
+ // HTTP server port (default 9966)
13
13
  httpPort?: number;
14
14
  // Whether to accept non-localhost HTTP connections
15
15
  lanEnabled?: boolean;
@@ -79,7 +79,7 @@ export interface ConversationMessage {
79
79
  export interface RpcMessage {
80
80
  method: string;
81
81
  params: Record<string, unknown>;
82
- sessionToken?: string;
83
- /** Trusted localhost request — skip session validation. */
82
+ clientToken?: string;
83
+ /** Trusted localhost request — skip client validation. */
84
84
  localhost?: boolean;
85
85
  }
@@ -1,29 +1,46 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { getAgentInstructions } from "../src/agents/shared-prompt.js";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ import { fileURLToPath } from "url";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const templatePath = path.join(__dirname, "..", "src", "agents", "agent-instructions.md");
9
+ const template = fs.readFileSync(templatePath, "utf-8");
10
+
11
+ /** Minimal replica of getAgentInstructions that doesn't need host.json */
12
+ function buildInstructions(taskId: string, skipPermissions?: boolean): string {
13
+ let instructions = template
14
+ .replace(/\{\{PORT\}\}/g, "9966")
15
+ .replace(/\{\{TASK_ID\}\}/g, taskId);
16
+ if (skipPermissions) {
17
+ instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
18
+ }
19
+ return instructions;
20
+ }
4
21
 
5
22
  describe("getAgentInstructions", () => {
6
23
  it("includes Permissions section by default", () => {
7
- const result = getAgentInstructions("test-task-id");
24
+ const result = buildInstructions("test-task-id");
8
25
  assert.match(result, /## Permissions/);
9
26
  assert.match(result, /PALMIER_PERMISSION/);
10
27
  });
11
28
 
12
29
  it("strips Permissions section when skipPermissions is true", () => {
13
- const result = getAgentInstructions("test-task-id", true);
30
+ const result = buildInstructions("test-task-id", true);
14
31
  assert.doesNotMatch(result, /## Permissions/);
15
32
  assert.doesNotMatch(result, /PALMIER_PERMISSION/);
16
33
  });
17
34
 
18
35
  it("preserves other sections when Permissions is stripped", () => {
19
- const result = getAgentInstructions("test-task-id", true);
36
+ const result = buildInstructions("test-task-id", true);
20
37
  assert.match(result, /## Reporting Output/);
21
38
  assert.match(result, /## Completion/);
22
39
  assert.match(result, /## HTTP Endpoints/);
23
40
  });
24
41
 
25
42
  it("replaces template variables", () => {
26
- const result = getAgentInstructions("my-task-123");
43
+ const result = buildInstructions("my-task-123");
27
44
  assert.match(result, /my-task-123/);
28
45
  assert.doesNotMatch(result, /\{\{TASK_ID\}\}/);
29
46
  assert.doesNotMatch(result, /\{\{PORT\}\}/);
@@ -38,6 +38,11 @@ describe("parseReportFiles", () => {
38
38
  it("trims whitespace from file names", () => {
39
39
  assert.deepEqual(parseReportFiles("[PALMIER_REPORT] report.md "), ["report.md"]);
40
40
  });
41
+
42
+ it("ignores placeholder examples from echoed prompt", () => {
43
+ const output = "[PALMIER_REPORT] <filename>\n[PALMIER_REPORT] actual-report.md";
44
+ assert.deepEqual(parseReportFiles(output), ["actual-report.md"]);
45
+ });
41
46
  });
42
47
 
43
48
  describe("parsePermissions", () => {
@@ -57,5 +62,12 @@ describe("parsePermissions", () => {
57
62
  it("returns empty array when no permissions", () => {
58
63
  assert.deepEqual(parsePermissions("no permissions"), []);
59
64
  });
65
+
66
+ it("ignores placeholder examples from echoed prompt", () => {
67
+ const output = "[PALMIER_PERMISSION] <tool_name> | <description>\n[PALMIER_PERMISSION] Read | Read files";
68
+ const perms = parsePermissions(output);
69
+ assert.equal(perms.length, 1);
70
+ assert.deepEqual(perms[0], { name: "Read", description: "Read files" });
71
+ });
60
72
  });
61
73
 
@@ -1,4 +0,0 @@
1
- export declare function sessionsListCommand(): Promise<void>;
2
- export declare function sessionsRevokeCommand(token: string): Promise<void>;
3
- export declare function sessionsRevokeAllCommand(): Promise<void>;
4
- //# sourceMappingURL=sessions.d.ts.map
@@ -1,27 +0,0 @@
1
- import { loadSessions, revokeSession, revokeAllSessions } from "../session-store.js";
2
- export async function sessionsListCommand() {
3
- const sessions = loadSessions();
4
- if (sessions.length === 0) {
5
- console.log("No active sessions.");
6
- return;
7
- }
8
- console.log(`${sessions.length} active session(s):\n`);
9
- for (const s of sessions) {
10
- const label = s.label ? ` (${s.label})` : "";
11
- console.log(` ${s.token}${label} created ${s.createdAt}`);
12
- }
13
- }
14
- export async function sessionsRevokeCommand(token) {
15
- if (revokeSession(token)) {
16
- console.log("Session revoked.");
17
- }
18
- else {
19
- console.error("Session not found.");
20
- process.exit(1);
21
- }
22
- }
23
- export async function sessionsRevokeAllCommand() {
24
- const count = revokeAllSessions();
25
- console.log(`Revoked ${count} session(s).`);
26
- }
27
- //# sourceMappingURL=sessions.js.map
@@ -1,12 +0,0 @@
1
- export interface SessionEntry {
2
- token: string;
3
- createdAt: string;
4
- label?: string;
5
- }
6
- export declare function loadSessions(): SessionEntry[];
7
- export declare function addSession(label?: string): SessionEntry;
8
- export declare function revokeSession(token: string): boolean;
9
- export declare function revokeAllSessions(): number;
10
- export declare function validateSession(token: string): boolean;
11
- export declare function hasSessions(): boolean;
12
- //# sourceMappingURL=session-store.d.ts.map
@@ -1,57 +0,0 @@
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
- const SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
6
- function readFile() {
7
- try {
8
- if (!fs.existsSync(SESSIONS_FILE))
9
- return [];
10
- const raw = fs.readFileSync(SESSIONS_FILE, "utf-8");
11
- return JSON.parse(raw);
12
- }
13
- catch {
14
- return [];
15
- }
16
- }
17
- function writeFile(sessions) {
18
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
19
- fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2), "utf-8");
20
- }
21
- export function loadSessions() {
22
- return readFile();
23
- }
24
- export function addSession(label) {
25
- const sessions = readFile();
26
- const entry = {
27
- token: randomBytes(32).toString("hex"),
28
- createdAt: new Date().toISOString(),
29
- ...(label ? { label } : {}),
30
- };
31
- sessions.push(entry);
32
- writeFile(sessions);
33
- return entry;
34
- }
35
- export function revokeSession(token) {
36
- const sessions = readFile();
37
- const idx = sessions.findIndex((s) => s.token === token);
38
- if (idx === -1)
39
- return false;
40
- sessions.splice(idx, 1);
41
- writeFile(sessions);
42
- return true;
43
- }
44
- export function revokeAllSessions() {
45
- const sessions = readFile();
46
- const count = sessions.length;
47
- writeFile([]);
48
- return count;
49
- }
50
- export function validateSession(token) {
51
- const sessions = readFile();
52
- return sessions.some((s) => s.token === token);
53
- }
54
- export function hasSessions() {
55
- return readFile().length > 0;
56
- }
57
- //# sourceMappingURL=session-store.js.map
@@ -1,29 +0,0 @@
1
- import { loadSessions, revokeSession, revokeAllSessions } from "../session-store.js";
2
-
3
- export async function sessionsListCommand(): Promise<void> {
4
- const sessions = loadSessions();
5
- if (sessions.length === 0) {
6
- console.log("No active sessions.");
7
- return;
8
- }
9
-
10
- console.log(`${sessions.length} active session(s):\n`);
11
- for (const s of sessions) {
12
- const label = s.label ? ` (${s.label})` : "";
13
- console.log(` ${s.token}${label} created ${s.createdAt}`);
14
- }
15
- }
16
-
17
- export async function sessionsRevokeCommand(token: string): Promise<void> {
18
- if (revokeSession(token)) {
19
- console.log("Session revoked.");
20
- } else {
21
- console.error("Session not found.");
22
- process.exit(1);
23
- }
24
- }
25
-
26
- export async function sessionsRevokeAllCommand(): Promise<void> {
27
- const count = revokeAllSessions();
28
- console.log(`Revoked ${count} session(s).`);
29
- }
@@ -1,68 +0,0 @@
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 SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
7
-
8
- export interface SessionEntry {
9
- token: string;
10
- createdAt: string;
11
- label?: string;
12
- }
13
-
14
- function readFile(): SessionEntry[] {
15
- try {
16
- if (!fs.existsSync(SESSIONS_FILE)) return [];
17
- const raw = fs.readFileSync(SESSIONS_FILE, "utf-8");
18
- return JSON.parse(raw) as SessionEntry[];
19
- } catch {
20
- return [];
21
- }
22
- }
23
-
24
- function writeFile(sessions: SessionEntry[]): void {
25
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
26
- fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2), "utf-8");
27
- }
28
-
29
- export function loadSessions(): SessionEntry[] {
30
- return readFile();
31
- }
32
-
33
- export function addSession(label?: string): SessionEntry {
34
- const sessions = readFile();
35
- const entry: SessionEntry = {
36
- token: randomBytes(32).toString("hex"),
37
- createdAt: new Date().toISOString(),
38
- ...(label ? { label } : {}),
39
- };
40
- sessions.push(entry);
41
- writeFile(sessions);
42
- return entry;
43
- }
44
-
45
- export function revokeSession(token: string): boolean {
46
- const sessions = readFile();
47
- const idx = sessions.findIndex((s) => s.token === token);
48
- if (idx === -1) return false;
49
- sessions.splice(idx, 1);
50
- writeFile(sessions);
51
- return true;
52
- }
53
-
54
- export function revokeAllSessions(): number {
55
- const sessions = readFile();
56
- const count = sessions.length;
57
- writeFile([]);
58
- return count;
59
- }
60
-
61
- export function validateSession(token: string): boolean {
62
- const sessions = readFile();
63
- return sessions.some((s) => s.token === token);
64
- }
65
-
66
- export function hasSessions(): boolean {
67
- return readFile().length > 0;
68
- }