palmier 0.7.7 → 0.7.9

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 (106) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/agent.d.ts +3 -0
  3. package/dist/agents/agent.js +1 -1
  4. package/dist/agents/aider.d.ts +1 -0
  5. package/dist/agents/aider.js +1 -0
  6. package/dist/agents/claude.d.ts +1 -0
  7. package/dist/agents/claude.js +1 -0
  8. package/dist/agents/cline.d.ts +1 -0
  9. package/dist/agents/cline.js +1 -0
  10. package/dist/agents/codex.d.ts +1 -0
  11. package/dist/agents/codex.js +1 -0
  12. package/dist/agents/copilot.d.ts +1 -0
  13. package/dist/agents/copilot.js +1 -0
  14. package/dist/agents/cursor.d.ts +1 -0
  15. package/dist/agents/cursor.js +1 -0
  16. package/dist/agents/deepagents.d.ts +1 -0
  17. package/dist/agents/deepagents.js +1 -0
  18. package/dist/agents/droid.d.ts +1 -0
  19. package/dist/agents/droid.js +1 -0
  20. package/dist/agents/gemini.d.ts +1 -0
  21. package/dist/agents/gemini.js +1 -0
  22. package/dist/agents/goose.d.ts +1 -0
  23. package/dist/agents/goose.js +1 -0
  24. package/dist/agents/hermes.d.ts +1 -0
  25. package/dist/agents/hermes.js +1 -0
  26. package/dist/agents/kimi.d.ts +1 -0
  27. package/dist/agents/kimi.js +1 -0
  28. package/dist/agents/kiro.d.ts +1 -0
  29. package/dist/agents/kiro.js +1 -0
  30. package/dist/agents/openclaw.d.ts +1 -0
  31. package/dist/agents/openclaw.js +2 -2
  32. package/dist/agents/opencode.d.ts +1 -0
  33. package/dist/agents/opencode.js +1 -0
  34. package/dist/agents/qoder.d.ts +1 -0
  35. package/dist/agents/qoder.js +1 -0
  36. package/dist/agents/qwen.d.ts +1 -0
  37. package/dist/agents/qwen.js +1 -0
  38. package/dist/commands/pair.js +2 -2
  39. package/dist/mcp-tools.d.ts +2 -0
  40. package/dist/mcp-tools.js +20 -9
  41. package/dist/pending-requests.d.ts +30 -8
  42. package/dist/pending-requests.js +28 -15
  43. package/dist/platform/linux.js +11 -8
  44. package/dist/platform/windows.d.ts +5 -6
  45. package/dist/platform/windows.js +15 -12
  46. package/dist/pwa/assets/index-FP1Mipr6.js +120 -0
  47. package/dist/pwa/assets/index-bLTn8zBj.css +1 -0
  48. package/dist/pwa/assets/{web-CkWrlNwc.js → web-BpM3fNCn.js} +1 -1
  49. package/dist/pwa/assets/{web-lx34oBi7.js → web-CF-N8Di6.js} +1 -1
  50. package/dist/pwa/index.html +2 -2
  51. package/dist/pwa/service-worker.js +1 -1
  52. package/dist/rpc-handler.js +35 -24
  53. package/dist/task.js +1 -1
  54. package/dist/transports/http-transport.js +9 -8
  55. package/dist/types.d.ts +11 -6
  56. package/package.json +1 -1
  57. package/palmier-server/README.md +3 -3
  58. package/palmier-server/pwa/src/App.css +175 -28
  59. package/palmier-server/pwa/src/App.tsx +1 -0
  60. package/palmier-server/pwa/src/components/HostMenu.tsx +7 -2
  61. package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +46 -0
  62. package/palmier-server/pwa/src/components/RunDetailView.tsx +58 -15
  63. package/palmier-server/pwa/src/components/SessionComposer.tsx +147 -0
  64. package/palmier-server/pwa/src/components/{RunsView.tsx → SessionsView.tsx} +79 -45
  65. package/palmier-server/pwa/src/components/TabBar.tsx +17 -10
  66. package/palmier-server/pwa/src/components/TaskCard.tsx +33 -35
  67. package/palmier-server/pwa/src/components/TaskForm.tsx +275 -349
  68. package/palmier-server/pwa/src/components/TasksView.tsx +172 -0
  69. package/palmier-server/pwa/src/constants.ts +1 -1
  70. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +16 -8
  71. package/palmier-server/pwa/src/draftGuard.ts +24 -0
  72. package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +102 -0
  73. package/palmier-server/pwa/src/pages/Dashboard.tsx +343 -37
  74. package/palmier-server/pwa/src/types.ts +5 -14
  75. package/palmier-server/spec.md +39 -26
  76. package/src/agents/agent.ts +5 -1
  77. package/src/agents/aider.ts +1 -0
  78. package/src/agents/claude.ts +1 -0
  79. package/src/agents/cline.ts +1 -0
  80. package/src/agents/codex.ts +1 -0
  81. package/src/agents/copilot.ts +1 -0
  82. package/src/agents/cursor.ts +1 -0
  83. package/src/agents/deepagents.ts +1 -0
  84. package/src/agents/droid.ts +1 -0
  85. package/src/agents/gemini.ts +1 -0
  86. package/src/agents/goose.ts +1 -0
  87. package/src/agents/hermes.ts +1 -0
  88. package/src/agents/kimi.ts +1 -0
  89. package/src/agents/kiro.ts +1 -0
  90. package/src/agents/openclaw.ts +2 -2
  91. package/src/agents/opencode.ts +1 -0
  92. package/src/agents/qoder.ts +1 -0
  93. package/src/agents/qwen.ts +1 -0
  94. package/src/commands/pair.ts +2 -2
  95. package/src/mcp-tools.ts +22 -9
  96. package/src/pending-requests.ts +47 -15
  97. package/src/platform/linux.ts +10 -8
  98. package/src/platform/windows.ts +15 -12
  99. package/src/rpc-handler.ts +39 -26
  100. package/src/task.ts +1 -1
  101. package/src/transports/http-transport.ts +9 -8
  102. package/src/types.ts +10 -8
  103. package/test/pairing.test.ts +2 -2
  104. package/dist/pwa/assets/index-B-ByUHPS.css +0 -1
  105. package/dist/pwa/assets/index-Bt8Hhaw3.js +0 -118
  106. package/palmier-server/pwa/src/components/TaskListView.tsx +0 -431
@@ -44,6 +44,9 @@ export interface AgentTool {
44
44
  * If false, the permissions section is omitted from agent instructions. */
45
45
  supportsPermissions: boolean;
46
46
 
47
+ /** Whether this agent supports yolo mode (auto-approve all tools). */
48
+ supportsYolo: boolean;
49
+
47
50
  /** Detect whether the agent CLI is available and perform any agent-specific
48
51
  * initialization. Returns true if the agent was detected and initialized successfully. */
49
52
  init(): Promise<boolean>;
@@ -93,6 +96,7 @@ export interface DetectedAgent {
93
96
  key: string;
94
97
  label: string;
95
98
  supportsPermissions: boolean;
99
+ supportsYolo: boolean;
96
100
  }
97
101
 
98
102
  export async function detectAgents(): Promise<DetectedAgent[]> {
@@ -100,7 +104,7 @@ export async function detectAgents(): Promise<DetectedAgent[]> {
100
104
  for (const [key, agent] of Object.entries(agentRegistry)) {
101
105
  const label = agentLabels[key] ?? key;
102
106
  const ok = await agent.init();
103
- if (ok) detected.push({ key, label, supportsPermissions: agent.supportsPermissions });
107
+ if (ok) detected.push({ key, label, supportsPermissions: agent.supportsPermissions, supportsYolo: agent.supportsYolo });
104
108
  }
105
109
  return detected;
106
110
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Aider implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "aider", args: ["--message", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class ClaudeAgent implements AgentTool {
8
8
  supportsPermissions = true;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "claude", args: ["-p", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Cline implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "cline ", args: ["--yolo", "-p", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CodexAgent implements AgentTool {
8
8
  supportsPermissions = true;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "codex", args: ["exec", "--skip-git-repo-check", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CopilotAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "copilot", args: ["-p", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Cursor implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "cursor", args: ["-p", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class DeepAgents implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "deepagents", args: ["--non-interactive", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class DroidAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "droid", args: ["exec", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class GeminiAgent implements AgentTool {
8
8
  supportsPermissions = true;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "gemini", args: ["--prompt", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class GooseAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "goose", args: ["run", "--text", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Hermes implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "hermes", args: ["chat", "-q", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class KimiAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "kimi", args: ["-p", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Kiro implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "kiro-cli", args: ["--no-interactive", prompt] };
11
12
  }
@@ -5,13 +5,13 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
 
6
6
  export class OpenClawAgent implements AgentTool {
7
7
  supportsPermissions = false;
8
+ supportsYolo = false;
8
9
  getPromptCommandLine(prompt: string): CommandLine {
9
10
  return { command: "openclaw", args: ["agent", "--local", "--agent", "main", "--message", prompt] };
10
11
  }
11
12
 
12
13
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
13
- const yolo = extraPermissions === "yolo";
14
- const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
14
+ const prompt = followupPrompt ?? getAgentInstructions(task, true);
15
15
  // OpenClaw does not support stdin as prompt.
16
16
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
17
17
 
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class OpenCodeAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "opencode", args: ["run", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class Qoder implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "qodercli", args: ["-p", prompt] };
11
12
  }
@@ -6,6 +6,7 @@ import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class QwenAgent implements AgentTool {
8
8
  supportsPermissions = false;
9
+ supportsYolo = true;
9
10
  getPromptCommandLine(prompt: string): CommandLine {
10
11
  return { command: "qwen", args: ["-p", prompt] };
11
12
  }
@@ -8,7 +8,7 @@ import type { HostConfig } from "../types.js";
8
8
  const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
9
9
  const CODE_LENGTH = 6;
10
10
 
11
- export const PAIRING_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
11
+ export const PAIRING_EXPIRY_MS = 60 * 1000; // 1 minute
12
12
 
13
13
  export function generatePairingCode(): string {
14
14
  const bytes = new Uint8Array(CODE_LENGTH);
@@ -84,7 +84,7 @@ export async function pairCommand(): Promise<void> {
84
84
  console.log("");
85
85
  console.log(` ${code}`);
86
86
  console.log("");
87
- console.log("Code expires in 5 minutes.");
87
+ console.log("Code expires in 1 minute.");
88
88
 
89
89
  // NATS pairing (server mode)
90
90
  const nc = await connectNats(config);
package/src/mcp-tools.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { StringCodec, type NatsConnection } from "nats";
2
2
  import { registerPending } from "./pending-requests.js";
3
3
  import { getCapabilityDevice } from "./device-capabilities.js";
4
- import { getNotifications } from "./notification-store.js";
5
- import { getSmsMessages } from "./sms-store.js";
4
+ import { getNotifications, onNotificationsChanged } from "./notification-store.js";
5
+ import { getSmsMessages, onSmsChanged } from "./sms-store.js";
6
6
  import type { HostConfig } from "./types.js";
7
7
 
8
8
  export class ToolError extends Error {
@@ -65,7 +65,7 @@ const requestInputTool: ToolDefinition = {
65
65
  "Request input from the user.",
66
66
  "The request blocks until the user responds.",
67
67
  'Response: `{"values": ["answer1", "answer2"]}` on success, or `{"aborted": true}` if the user declines.',
68
- "When you need information from the user (credentials, answers to questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout, even in a non-interactive environment — use this endpoint instead.",
68
+ "When you need information from the user (credentials, answers to questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout, even in a non-interactive environment — use this instead.",
69
69
  ],
70
70
  inputSchema: {
71
71
  type: "object",
@@ -84,13 +84,18 @@ const requestInputTool: ToolDefinition = {
84
84
  const { description, questions } = args as { description?: string; questions: string[] };
85
85
  if (!questions?.length) throw new ToolError("questions is required", 400);
86
86
 
87
- const pendingPromise = registerPending(ctx.sessionId, "input", questions);
87
+ const pendingPromise = registerPending(ctx.sessionId, "input", questions, {
88
+ session_id: ctx.sessionId,
89
+ session_name: ctx.agentName,
90
+ description,
91
+ input_questions: questions,
92
+ });
88
93
 
89
94
  await ctx.publishEvent("_input", {
90
95
  event_type: "input-request",
91
96
  host_id: ctx.config.hostId,
92
97
  session_id: ctx.sessionId,
93
- agent_name: ctx.agentName,
98
+ session_name: ctx.agentName,
94
99
  description,
95
100
  input_questions: questions,
96
101
  });
@@ -131,13 +136,17 @@ const requestConfirmationTool: ToolDefinition = {
131
136
  const { description } = args as { description: string };
132
137
  if (!description) throw new ToolError("description is required", 400);
133
138
 
134
- const pendingPromise = registerPending(ctx.sessionId, "confirmation");
139
+ const pendingPromise = registerPending(ctx.sessionId, "confirmation", undefined, {
140
+ session_id: ctx.sessionId,
141
+ session_name: ctx.agentName,
142
+ description,
143
+ });
135
144
 
136
145
  await ctx.publishEvent("_confirm", {
137
146
  event_type: "confirm-request",
138
147
  host_id: ctx.config.hostId,
139
148
  session_id: ctx.sessionId,
140
- agent_name: ctx.agentName,
149
+ session_name: ctx.agentName,
141
150
  description,
142
151
  });
143
152
 
@@ -159,7 +168,7 @@ const deviceGeolocationTool: ToolDefinition = {
159
168
  name: "device-geolocation",
160
169
  description: [
161
170
  "Get the GPS location of the user's mobile device.",
162
- "When you need the user's real-time location, use this endpoint.",
171
+ "When you need the user's real-time location, use this.",
163
172
  "Blocks until the device responds (up to 30 seconds).",
164
173
  'Response: `{"latitude": ..., "longitude": ..., "accuracy": ..., "timestamp": ...}` on success, or `{"error": "..."}` on failure.',
165
174
  ],
@@ -661,7 +670,7 @@ const sendEmailTool: ToolDefinition = {
661
670
  name: "send-email",
662
671
  description: [
663
672
  "Send an email from the user's mobile device.",
664
- "When you need to send an email, use this tool. The email app opens on the device with the draft pre-filled for the user to review and send.",
673
+ "When you need to send an email, use this. The email app opens on the device with the draft pre-filled for the user to review and send.",
665
674
  'Response: `{"ok": true}` on success, or `{"error": "..."}` on failure.',
666
675
  ],
667
676
  inputSchema: {
@@ -741,6 +750,8 @@ export interface ResourceDefinition {
741
750
  restPath: string;
742
751
  /** Return the current resource content. */
743
752
  read: () => unknown;
753
+ /** Register a listener for content changes. Returns an unsubscribe function. */
754
+ subscribe: (listener: () => void) => () => void;
744
755
  }
745
756
 
746
757
  const deviceNotificationsResource: ResourceDefinition = {
@@ -753,6 +764,7 @@ const deviceNotificationsResource: ResourceDefinition = {
753
764
  mimeType: "application/json",
754
765
  restPath: "/notifications",
755
766
  read: getNotifications,
767
+ subscribe: onNotificationsChanged,
756
768
  };
757
769
 
758
770
  const deviceSmsResource: ResourceDefinition = {
@@ -765,6 +777,7 @@ const deviceSmsResource: ResourceDefinition = {
765
777
  mimeType: "application/json",
766
778
  restPath: "/sms-messages",
767
779
  read: getSmsMessages,
780
+ subscribe: onSmsChanged,
768
781
  };
769
782
 
770
783
  export const agentResources: ResourceDefinition[] = [deviceNotificationsResource, deviceSmsResource];
@@ -1,30 +1,44 @@
1
1
  import type { RequiredPermission } from "./types.js";
2
2
 
3
+ export interface PendingRequestMeta {
4
+ /** Doubles as task_id for permission-type entries (the key the task uses). */
5
+ session_id?: string;
6
+ /** Human-readable label for whoever opened the prompt — agent name for
7
+ * confirm/input, task name for permission. */
8
+ session_name?: string;
9
+ description?: string;
10
+ input_questions?: string[];
11
+ }
12
+
3
13
  export interface PendingRequest {
4
14
  type: "confirmation" | "permission" | "input";
5
15
  resolve: (value: string[]) => void;
6
16
  /** Permission list (for 'permission') or input descriptions (for 'input'). */
7
17
  params?: RequiredPermission[] | string[];
18
+ /** Display context for PWAs that connect while this request is already open. */
19
+ meta?: PendingRequestMeta;
8
20
  }
9
21
 
10
22
  const pending = new Map<string, PendingRequest>();
11
23
 
12
24
  /**
13
- * Register a pending request for a task. Returns a Promise that resolves
14
- * when `resolvePending` is called with the user's response.
15
- * Only one pending request per task at a time.
25
+ * Register a pending request keyed by either a sessionId (confirmation / input)
26
+ * or a taskId (permission). The `meta` is surfaced to PWAs that connect after
27
+ * the request was opened, so their modals can render without replaying events.
28
+ * Only one pending request per key at a time.
16
29
  */
17
30
  export function registerPending(
18
- taskId: string,
31
+ key: string,
19
32
  type: PendingRequest["type"],
20
33
  params?: PendingRequest["params"],
34
+ meta?: PendingRequestMeta,
21
35
  ): Promise<string[]> {
22
- if (pending.has(taskId)) {
23
- return Promise.reject(new Error(`Task ${taskId} already has a pending request`));
36
+ if (pending.has(key)) {
37
+ return Promise.reject(new Error(`Key ${key} already has a pending request`));
24
38
  }
25
39
 
26
40
  return new Promise<string[]>((resolve) => {
27
- pending.set(taskId, { type, resolve, params });
41
+ pending.set(key, { type, resolve, params, meta });
28
42
  });
29
43
  }
30
44
 
@@ -32,24 +46,42 @@ export function registerPending(
32
46
  * Resolve a pending request with the user's response.
33
47
  * Returns true if a pending request was found and resolved.
34
48
  */
35
- export function resolvePending(taskId: string, value: string[]): boolean {
36
- const entry = pending.get(taskId);
49
+ export function resolvePending(key: string, value: string[]): boolean {
50
+ const entry = pending.get(key);
37
51
  if (!entry) return false;
38
- pending.delete(taskId);
52
+ pending.delete(key);
39
53
  entry.resolve(value);
40
54
  return true;
41
55
  }
42
56
 
43
57
  /**
44
- * Get the current pending request for a task (if any).
58
+ * Get the current pending request for a key (if any).
45
59
  */
46
- export function getPending(taskId: string): PendingRequest | undefined {
47
- return pending.get(taskId);
60
+ export function getPending(key: string): PendingRequest | undefined {
61
+ return pending.get(key);
48
62
  }
49
63
 
50
64
  /**
51
65
  * Remove a pending request without resolving it.
52
66
  */
53
- export function removePending(taskId: string): void {
54
- pending.delete(taskId);
67
+ export function removePending(key: string): void {
68
+ pending.delete(key);
69
+ }
70
+
71
+ /**
72
+ * List all currently-pending requests, stripped of the unserializable `resolve`
73
+ * callback. Used by `host.info` so the PWA can seed its modal state on connect.
74
+ */
75
+ export function listPending(): Array<{
76
+ key: string;
77
+ type: PendingRequest["type"];
78
+ params?: PendingRequest["params"];
79
+ meta?: PendingRequestMeta;
80
+ }> {
81
+ return [...pending.entries()].map(([key, entry]) => ({
82
+ key,
83
+ type: entry.type,
84
+ params: entry.params,
85
+ meta: entry.meta,
86
+ }));
55
87
  }
@@ -196,15 +196,17 @@ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
196
196
  fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
197
197
  daemonReload();
198
198
 
199
- // Only create and enable a timer if triggers exist and are enabled
200
- if (!task.frontmatter.triggers_enabled) return;
201
- const triggers = task.frontmatter.triggers || [];
199
+ // Only create and enable a timer if the schedule exists and is enabled
200
+ if (!task.frontmatter.schedule_enabled) return;
201
+ const scheduleType = task.frontmatter.schedule_type;
202
+ const scheduleValues = task.frontmatter.schedule_values;
203
+ if (!scheduleType || !scheduleValues?.length) return;
202
204
  const onCalendarLines: string[] = [];
203
- for (const trigger of triggers) {
204
- if (trigger.type === "cron") {
205
- onCalendarLines.push(`OnCalendar=${cronToOnCalendar(trigger.value)}`);
206
- } else if (trigger.type === "once") {
207
- onCalendarLines.push(`OnActiveSec=${trigger.value}`);
205
+ for (const value of scheduleValues) {
206
+ if (scheduleType === "crons") {
207
+ onCalendarLines.push(`OnCalendar=${cronToOnCalendar(value)}`);
208
+ } else if (scheduleType === "specific_times") {
209
+ onCalendarLines.push(`OnActiveSec=${value}`);
208
210
  }
209
211
  }
210
212
 
@@ -14,22 +14,23 @@ const DAEMON_TASK_NAME = "PalmierDaemon";
14
14
  const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
15
15
 
16
16
  /**
17
- * Convert a cron expression or "once" trigger to Task Scheduler XML trigger elements.
17
+ * Convert a single schedule value to a Task Scheduler XML trigger element.
18
18
  *
19
- * Only these cron patterns (produced by the PWA UI) are handled:
19
+ * `specific_times` values are ISO datetime strings like "2026-03-28T09:00".
20
+ *
21
+ * `crons` values are cron expressions. Only these patterns (produced by the PWA UI) are handled:
20
22
  * hourly: "0 * * * *"
21
23
  * daily: "MM HH * * *"
22
24
  * weekly: "MM HH * * D"
23
25
  * monthly: "MM HH D * *"
24
26
  */
25
- export function triggerToXml(trigger: { type: string; value: string }): string {
26
- if (trigger.type === "once") {
27
- // ISO datetime "2026-03-28T09:00"
28
- return `<TimeTrigger><StartBoundary>${trigger.value}:00</StartBoundary></TimeTrigger>`;
27
+ export function scheduleValueToXml(scheduleType: "crons" | "specific_times", value: string): string {
28
+ if (scheduleType === "specific_times") {
29
+ return `<TimeTrigger><StartBoundary>${value}:00</StartBoundary></TimeTrigger>`;
29
30
  }
30
31
 
31
- const parts = trigger.value.trim().split(/\s+/);
32
- if (parts.length !== 5) throw new Error(`Invalid cron expression: ${trigger.value}`);
32
+ const parts = value.trim().split(/\s+/);
33
+ if (parts.length !== 5) throw new Error(`Invalid cron expression: ${value}`);
33
34
  const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
34
35
  const st = `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:00`;
35
36
  // StartBoundary needs a full date; use a past date as the anchor
@@ -193,12 +194,14 @@ export class WindowsPlatform implements PlatformService {
193
194
 
194
195
  // Build trigger XML elements
195
196
  const triggerElements: string[] = [];
196
- if (task.frontmatter.triggers_enabled) {
197
- for (const trigger of task.frontmatter.triggers ?? []) {
197
+ const scheduleType = task.frontmatter.schedule_type;
198
+ const scheduleValues = task.frontmatter.schedule_values;
199
+ if (task.frontmatter.schedule_enabled && scheduleType && scheduleValues?.length) {
200
+ for (const value of scheduleValues) {
198
201
  try {
199
- triggerElements.push(triggerToXml(trigger));
202
+ triggerElements.push(scheduleValueToXml(scheduleType, value));
200
203
  } catch (err) {
201
- console.error(`Invalid trigger: ${err}`);
204
+ console.error(`Invalid schedule value: ${err}`);
202
205
  }
203
206
  }
204
207
  }
@@ -4,7 +4,7 @@ import * as path from "path";
4
4
  import { spawn, type ChildProcess } from "child_process";
5
5
  import { type NatsConnection } from "nats";
6
6
  import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
7
- import { resolvePending, getPending } from "./pending-requests.js";
7
+ import { resolvePending, getPending, listPending } from "./pending-requests.js";
8
8
  import { getPlatform } from "./platform/index.js";
9
9
  import { spawnCommand } from "./spawn-command.js";
10
10
  import crossSpawn from "cross-spawn";
@@ -142,13 +142,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
142
142
  function flattenTask(task: ParsedTask) {
143
143
  const taskDir = getTaskDir(config.projectRoot, task.frontmatter.id);
144
144
  const status = readTaskStatus(taskDir);
145
- const pending = getPending(task.frontmatter.id);
146
145
  return {
147
146
  ...task.frontmatter,
148
- status: status ? {
149
- ...status,
150
- ...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
151
- } : undefined,
147
+ status: status ?? undefined,
152
148
  };
153
149
  }
154
150
 
@@ -161,22 +157,28 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
161
157
  }
162
158
 
163
159
  switch (request.method) {
164
- case "task.list": {
165
- const tasks = listTasks(config.projectRoot);
160
+ case "host.info": {
161
+ // Bootstrap metadata the PWA needs on connect, independent of which tab
162
+ // is active. Includes any prompts already waiting so a reconnecting
163
+ // PWA can render their modals without replaying events.
166
164
  const capabilities: Record<string, string | null> = {};
167
165
  for (const cap of ["location", "notifications", "sms", "contacts", "calendar", "alert", "battery", "dnd"] as const) {
168
166
  capabilities[cap] = getCapabilityDevice(cap)?.clientToken ?? null;
169
167
  }
170
168
  return {
171
- tasks: tasks.map((task) => flattenTask(task)),
172
169
  agents: config.agents ?? [],
173
170
  version: currentVersion,
174
171
  host_platform: process.platform,
175
- location_client_token: capabilities.location,
176
172
  capability_tokens: capabilities,
173
+ pending_prompts: listPending(),
177
174
  };
178
175
  }
179
176
 
177
+ case "task.list": {
178
+ const tasks = listTasks(config.projectRoot);
179
+ return { tasks: tasks.map((task) => flattenTask(task)) };
180
+ }
181
+
180
182
  case "task.get": {
181
183
  const params = request.params as { id: string };
182
184
  const taskDir = getTaskDir(config.projectRoot, params.id);
@@ -192,8 +194,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
192
194
  const params = request.params as {
193
195
  user_prompt: string;
194
196
  agent: string;
195
- triggers?: Array<{ type: "cron" | "once"; value: string }>;
196
- triggers_enabled?: boolean;
197
+ schedule_type?: "crons" | "specific_times";
198
+ schedule_values?: string[];
199
+ schedule_enabled?: boolean;
197
200
  requires_confirmation?: boolean;
198
201
  yolo_mode?: boolean;
199
202
  foreground_mode?: boolean;
@@ -212,9 +215,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
212
215
  name,
213
216
  user_prompt: params.user_prompt,
214
217
  agent: params.agent,
215
- triggers: params.triggers ?? [],
216
- triggers_enabled: params.triggers_enabled ?? true,
218
+ schedule_enabled: params.schedule_enabled ?? true,
217
219
  requires_confirmation: params.requires_confirmation ?? true,
220
+ ...(params.schedule_type && params.schedule_values?.length
221
+ ? { schedule_type: params.schedule_type, schedule_values: params.schedule_values }
222
+ : {}),
218
223
  ...(params.yolo_mode ? { yolo_mode: true } : {}),
219
224
  ...(params.foreground_mode ? { foreground_mode: true } : {}),
220
225
  ...(params.command ? { command: params.command } : {}),
@@ -233,8 +238,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
233
238
  id: string;
234
239
  user_prompt?: string;
235
240
  agent?: string;
236
- triggers?: Array<{ type: "cron" | "once"; value: string }>;
237
- triggers_enabled?: boolean;
241
+ schedule_type?: "crons" | "specific_times" | null;
242
+ schedule_values?: string[] | null;
243
+ schedule_enabled?: boolean;
238
244
  requires_confirmation?: boolean;
239
245
  yolo_mode?: boolean;
240
246
  foreground_mode?: boolean;
@@ -251,8 +257,21 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
251
257
  // Merge updates
252
258
  if (params.user_prompt !== undefined) existing.frontmatter.user_prompt = params.user_prompt;
253
259
  if (params.agent !== undefined) existing.frontmatter.agent = params.agent;
254
- if (params.triggers !== undefined) existing.frontmatter.triggers = params.triggers;
255
- if (params.triggers_enabled !== undefined) existing.frontmatter.triggers_enabled = params.triggers_enabled;
260
+ if (params.schedule_type !== undefined) {
261
+ if (params.schedule_type) {
262
+ existing.frontmatter.schedule_type = params.schedule_type;
263
+ } else {
264
+ delete existing.frontmatter.schedule_type;
265
+ }
266
+ }
267
+ if (params.schedule_values !== undefined) {
268
+ if (params.schedule_values && params.schedule_values.length > 0) {
269
+ existing.frontmatter.schedule_values = params.schedule_values;
270
+ } else {
271
+ delete existing.frontmatter.schedule_values;
272
+ }
273
+ }
274
+ if (params.schedule_enabled !== undefined) existing.frontmatter.schedule_enabled = params.schedule_enabled;
256
275
  if (params.requires_confirmation !== undefined)
257
276
  existing.frontmatter.requires_confirmation = params.requires_confirmation;
258
277
  if (params.yolo_mode !== undefined) {
@@ -312,8 +331,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
312
331
  name,
313
332
  user_prompt: params.user_prompt,
314
333
  agent: params.agent,
315
- triggers: [],
316
- triggers_enabled: false,
334
+ schedule_enabled: false,
317
335
  requires_confirmation: params.requires_confirmation ?? false,
318
336
  ...(params.yolo_mode ? { yolo_mode: true } : {}),
319
337
  ...(params.foreground_mode ? { foreground_mode: true } : {}),
@@ -537,12 +555,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
537
555
  if (!status) {
538
556
  return { task_id: params.id, error: "No status found" };
539
557
  }
540
- const pending = getPending(params.id);
541
- return {
542
- task_id: params.id,
543
- ...status,
544
- ...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
545
- };
558
+ return { task_id: params.id, ...status };
546
559
  }
547
560
 
548
561
  case "task.result": {
package/src/task.ts CHANGED
@@ -36,7 +36,7 @@ export function parseTaskContent(content: string): ParsedTask {
36
36
 
37
37
  frontmatter.name ??= frontmatter.user_prompt?.slice(0, 60) ?? "";
38
38
  frontmatter.agent ??= "claude";
39
- frontmatter.triggers_enabled ??= true;
39
+ frontmatter.schedule_enabled ??= true;
40
40
 
41
41
  return { frontmatter };
42
42
  }