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.
- package/README.md +1 -1
- package/dist/agents/agent.d.ts +3 -0
- package/dist/agents/agent.js +1 -1
- package/dist/agents/aider.d.ts +1 -0
- package/dist/agents/aider.js +1 -0
- package/dist/agents/claude.d.ts +1 -0
- package/dist/agents/claude.js +1 -0
- package/dist/agents/cline.d.ts +1 -0
- package/dist/agents/cline.js +1 -0
- package/dist/agents/codex.d.ts +1 -0
- package/dist/agents/codex.js +1 -0
- package/dist/agents/copilot.d.ts +1 -0
- package/dist/agents/copilot.js +1 -0
- package/dist/agents/cursor.d.ts +1 -0
- package/dist/agents/cursor.js +1 -0
- package/dist/agents/deepagents.d.ts +1 -0
- package/dist/agents/deepagents.js +1 -0
- package/dist/agents/droid.d.ts +1 -0
- package/dist/agents/droid.js +1 -0
- package/dist/agents/gemini.d.ts +1 -0
- package/dist/agents/gemini.js +1 -0
- package/dist/agents/goose.d.ts +1 -0
- package/dist/agents/goose.js +1 -0
- package/dist/agents/hermes.d.ts +1 -0
- package/dist/agents/hermes.js +1 -0
- package/dist/agents/kimi.d.ts +1 -0
- package/dist/agents/kimi.js +1 -0
- package/dist/agents/kiro.d.ts +1 -0
- package/dist/agents/kiro.js +1 -0
- package/dist/agents/openclaw.d.ts +1 -0
- package/dist/agents/openclaw.js +2 -2
- package/dist/agents/opencode.d.ts +1 -0
- package/dist/agents/opencode.js +1 -0
- package/dist/agents/qoder.d.ts +1 -0
- package/dist/agents/qoder.js +1 -0
- package/dist/agents/qwen.d.ts +1 -0
- package/dist/agents/qwen.js +1 -0
- package/dist/commands/pair.js +2 -2
- package/dist/mcp-tools.d.ts +2 -0
- package/dist/mcp-tools.js +20 -9
- package/dist/pending-requests.d.ts +30 -8
- package/dist/pending-requests.js +28 -15
- package/dist/platform/linux.js +11 -8
- package/dist/platform/windows.d.ts +5 -6
- package/dist/platform/windows.js +15 -12
- package/dist/pwa/assets/index-FP1Mipr6.js +120 -0
- package/dist/pwa/assets/index-bLTn8zBj.css +1 -0
- package/dist/pwa/assets/{web-CkWrlNwc.js → web-BpM3fNCn.js} +1 -1
- package/dist/pwa/assets/{web-lx34oBi7.js → web-CF-N8Di6.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +35 -24
- package/dist/task.js +1 -1
- package/dist/transports/http-transport.js +9 -8
- package/dist/types.d.ts +11 -6
- package/package.json +1 -1
- package/palmier-server/README.md +3 -3
- package/palmier-server/pwa/src/App.css +175 -28
- package/palmier-server/pwa/src/App.tsx +1 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +7 -2
- package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +46 -0
- package/palmier-server/pwa/src/components/RunDetailView.tsx +58 -15
- package/palmier-server/pwa/src/components/SessionComposer.tsx +147 -0
- package/palmier-server/pwa/src/components/{RunsView.tsx → SessionsView.tsx} +79 -45
- package/palmier-server/pwa/src/components/TabBar.tsx +17 -10
- package/palmier-server/pwa/src/components/TaskCard.tsx +33 -35
- package/palmier-server/pwa/src/components/TaskForm.tsx +275 -349
- package/palmier-server/pwa/src/components/TasksView.tsx +172 -0
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +16 -8
- package/palmier-server/pwa/src/draftGuard.ts +24 -0
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +102 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +343 -37
- package/palmier-server/pwa/src/types.ts +5 -14
- package/palmier-server/spec.md +39 -26
- package/src/agents/agent.ts +5 -1
- package/src/agents/aider.ts +1 -0
- package/src/agents/claude.ts +1 -0
- package/src/agents/cline.ts +1 -0
- package/src/agents/codex.ts +1 -0
- package/src/agents/copilot.ts +1 -0
- package/src/agents/cursor.ts +1 -0
- package/src/agents/deepagents.ts +1 -0
- package/src/agents/droid.ts +1 -0
- package/src/agents/gemini.ts +1 -0
- package/src/agents/goose.ts +1 -0
- package/src/agents/hermes.ts +1 -0
- package/src/agents/kimi.ts +1 -0
- package/src/agents/kiro.ts +1 -0
- package/src/agents/openclaw.ts +2 -2
- package/src/agents/opencode.ts +1 -0
- package/src/agents/qoder.ts +1 -0
- package/src/agents/qwen.ts +1 -0
- package/src/commands/pair.ts +2 -2
- package/src/mcp-tools.ts +22 -9
- package/src/pending-requests.ts +47 -15
- package/src/platform/linux.ts +10 -8
- package/src/platform/windows.ts +15 -12
- package/src/rpc-handler.ts +39 -26
- package/src/task.ts +1 -1
- package/src/transports/http-transport.ts +9 -8
- package/src/types.ts +10 -8
- package/test/pairing.test.ts +2 -2
- package/dist/pwa/assets/index-B-ByUHPS.css +0 -1
- package/dist/pwa/assets/index-Bt8Hhaw3.js +0 -118
- package/palmier-server/pwa/src/components/TaskListView.tsx +0 -431
package/src/agents/agent.ts
CHANGED
|
@@ -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
|
}
|
package/src/agents/aider.ts
CHANGED
package/src/agents/claude.ts
CHANGED
package/src/agents/cline.ts
CHANGED
|
@@ -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
|
}
|
package/src/agents/codex.ts
CHANGED
|
@@ -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
|
}
|
package/src/agents/copilot.ts
CHANGED
|
@@ -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
|
}
|
package/src/agents/cursor.ts
CHANGED
package/src/agents/deepagents.ts
CHANGED
|
@@ -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
|
}
|
package/src/agents/droid.ts
CHANGED
package/src/agents/gemini.ts
CHANGED
|
@@ -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
|
}
|
package/src/agents/goose.ts
CHANGED
|
@@ -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
|
}
|
package/src/agents/hermes.ts
CHANGED
|
@@ -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
|
}
|
package/src/agents/kimi.ts
CHANGED
package/src/agents/kiro.ts
CHANGED
|
@@ -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
|
}
|
package/src/agents/openclaw.ts
CHANGED
|
@@ -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
|
|
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
|
|
package/src/agents/opencode.ts
CHANGED
|
@@ -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
|
}
|
package/src/agents/qoder.ts
CHANGED
package/src/agents/qwen.ts
CHANGED
package/src/commands/pair.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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];
|
package/src/pending-requests.ts
CHANGED
|
@@ -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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
31
|
+
key: string,
|
|
19
32
|
type: PendingRequest["type"],
|
|
20
33
|
params?: PendingRequest["params"],
|
|
34
|
+
meta?: PendingRequestMeta,
|
|
21
35
|
): Promise<string[]> {
|
|
22
|
-
if (pending.has(
|
|
23
|
-
return Promise.reject(new Error(`
|
|
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(
|
|
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(
|
|
36
|
-
const entry = pending.get(
|
|
49
|
+
export function resolvePending(key: string, value: string[]): boolean {
|
|
50
|
+
const entry = pending.get(key);
|
|
37
51
|
if (!entry) return false;
|
|
38
|
-
pending.delete(
|
|
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
|
|
58
|
+
* Get the current pending request for a key (if any).
|
|
45
59
|
*/
|
|
46
|
-
export function getPending(
|
|
47
|
-
return pending.get(
|
|
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(
|
|
54
|
-
pending.delete(
|
|
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
|
}
|
package/src/platform/linux.ts
CHANGED
|
@@ -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
|
|
200
|
-
if (!task.frontmatter.
|
|
201
|
-
const
|
|
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
|
|
204
|
-
if (
|
|
205
|
-
onCalendarLines.push(`OnCalendar=${cronToOnCalendar(
|
|
206
|
-
} else if (
|
|
207
|
-
onCalendarLines.push(`OnActiveSec=${
|
|
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
|
|
package/src/platform/windows.ts
CHANGED
|
@@ -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
|
|
17
|
+
* Convert a single schedule value to a Task Scheduler XML trigger element.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
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
|
|
26
|
-
if (
|
|
27
|
-
|
|
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 =
|
|
32
|
-
if (parts.length !== 5) throw new Error(`Invalid cron expression: ${
|
|
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
|
-
|
|
197
|
-
|
|
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(
|
|
202
|
+
triggerElements.push(scheduleValueToXml(scheduleType, value));
|
|
200
203
|
} catch (err) {
|
|
201
|
-
console.error(`Invalid
|
|
204
|
+
console.error(`Invalid schedule value: ${err}`);
|
|
202
205
|
}
|
|
203
206
|
}
|
|
204
207
|
}
|
package/src/rpc-handler.ts
CHANGED
|
@@ -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 "
|
|
165
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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.
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
39
|
+
frontmatter.schedule_enabled ??= true;
|
|
40
40
|
|
|
41
41
|
return { frontmatter };
|
|
42
42
|
}
|