palmier 0.1.8 → 0.2.0

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/src/systemd.ts CHANGED
@@ -1,232 +1,164 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { homedir } from "os";
4
- import { execSync } from "child_process";
5
- import type { AgentConfig } from "./types.js";
6
- import type { ParsedTask, TaskStatus } from "./types.js";
7
-
8
- const UNIT_DIR = path.join(homedir(), ".config", "systemd", "user");
9
-
10
- function getTimerName(taskId: string): string {
11
- return `palmier-task-${taskId}.timer`;
12
- }
13
-
14
- function getServiceName(taskId: string): string {
15
- return `palmier-task-${taskId}.service`;
16
- }
17
-
18
- /**
19
- * Convert a cron expression (5-field) to a systemd OnCalendar string.
20
- * Handles basic cron patterns: minute hour day-of-month month day-of-week
21
- */
22
- export function cronToOnCalendar(cron: string): string {
23
- const parts = cron.trim().split(/\s+/);
24
- if (parts.length !== 5) {
25
- throw new Error(`Invalid cron expression (expected 5 fields): ${cron}`);
26
- }
27
-
28
- const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
29
-
30
- // Map cron day-of-week names/numbers to systemd abbreviated names
31
- const dowMap: Record<string, string> = {
32
- "0": "Sun",
33
- "1": "Mon",
34
- "2": "Tue",
35
- "3": "Wed",
36
- "4": "Thu",
37
- "5": "Fri",
38
- "6": "Sat",
39
- "7": "Sun",
40
- };
41
-
42
- // Convert day-of-week
43
- let dow = dayOfWeek === "*" ? "*" : dayOfWeek;
44
- if (dowMap[dow]) {
45
- dow = dowMap[dow];
46
- }
47
-
48
- // Build OnCalendar string
49
- // Format: DayOfWeek Year-Month-Day Hour:Minute:Second
50
- const monthPart = month === "*" ? "*" : month.padStart(2, "0");
51
- const dayPart = dayOfMonth === "*" ? "*" : dayOfMonth.padStart(2, "0");
52
- const hourPart = hour === "*" ? "*" : hour.padStart(2, "0");
53
- const minutePart = minute === "*" ? "*" : minute.padStart(2, "0");
54
-
55
- if (dow === "*") {
56
- return `*-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
57
- }
58
-
59
- return `${dow} *-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
60
- }
61
-
62
- /**
63
- * Install a systemd user timer + service for a task.
64
- */
65
- export function installTaskTimer(config: AgentConfig, task: ParsedTask): void {
66
- fs.mkdirSync(UNIT_DIR, { recursive: true });
67
-
68
- const taskId = task.frontmatter.id;
69
- const serviceName = getServiceName(taskId);
70
- const timerName = getTimerName(taskId);
71
-
72
- // Determine the palmier binary path
73
- const palmierBin = process.argv[1] || "palmier";
74
-
75
- // Generate service unit
76
- const serviceContent = `[Unit]
77
- Description=Palmier Task: ${task.frontmatter.name || taskId}
78
-
79
- [Service]
80
- Type=oneshot
81
- ExecStart=${palmierBin} run ${taskId}
82
- WorkingDirectory=${config.projectRoot}
83
- Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
84
- `;
85
-
86
- // Generate timer unit with OnCalendar entries for each cron trigger
87
- const onCalendarLines: string[] = [];
88
- for (const trigger of task.frontmatter.triggers || []) {
89
- if (trigger.type === "cron") {
90
- onCalendarLines.push(`OnCalendar=${cronToOnCalendar(trigger.value)}`);
91
- } else if (trigger.type === "once") {
92
- // "once" triggers use OnActiveSec or a specific timestamp
93
- onCalendarLines.push(`OnActiveSec=${trigger.value}`);
94
- }
95
- }
96
-
97
- const timerContent = `[Unit]
98
- Description=Timer for Palmier Task: ${task.frontmatter.name || taskId}
99
-
100
- [Timer]
101
- ${onCalendarLines.join("\n")}
102
- Persistent=true
103
-
104
- [Install]
105
- WantedBy=timers.target
106
- `;
107
-
108
- // Write unit files
109
- fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
110
- fs.writeFileSync(path.join(UNIT_DIR, timerName), timerContent, "utf-8");
111
-
112
- // Reload and enable
113
- daemonReload();
114
-
115
- if (task.frontmatter.enabled) {
116
- execSync(`systemctl --user enable --now ${timerName}`, { stdio: "inherit" });
117
- } else {
118
- execSync(`systemctl --user enable ${timerName}`, { stdio: "inherit" });
119
- }
120
- }
121
-
122
- /**
123
- * Remove a task's systemd timer and service files.
124
- */
125
- export function removeTaskTimer(taskId: string): void {
126
- const timerName = getTimerName(taskId);
127
- const serviceName = getServiceName(taskId);
128
-
129
- // Stop and disable
130
- try {
131
- execSync(`systemctl --user stop ${timerName}`, { stdio: "inherit" });
132
- } catch {
133
- // Timer might not be running
134
- }
135
-
136
- try {
137
- execSync(`systemctl --user disable ${timerName}`, { stdio: "inherit" });
138
- } catch {
139
- // Timer might not be enabled
140
- }
141
-
142
- // Remove unit files
143
- const timerPath = path.join(UNIT_DIR, timerName);
144
- const servicePath = path.join(UNIT_DIR, serviceName);
145
-
146
- if (fs.existsSync(timerPath)) fs.unlinkSync(timerPath);
147
- if (fs.existsSync(servicePath)) fs.unlinkSync(servicePath);
148
-
149
- daemonReload();
150
- }
151
-
152
- /**
153
- * Query systemd for task status information.
154
- */
155
- export function getTaskStatus(taskId: string): TaskStatus {
156
- const timerName = getTimerName(taskId);
157
- const serviceName = getServiceName(taskId);
158
-
159
- let state: TaskStatus["state"] = "inactive";
160
- let lastRun: string | undefined;
161
- let lastResult: number | undefined;
162
- let nextRun: string | undefined;
163
-
164
- // Check if timer is active
165
- try {
166
- const activeState = execSync(`systemctl --user is-active ${timerName}`, {
167
- encoding: "utf-8",
168
- }).trim();
169
- if (activeState === "active") {
170
- state = "active";
171
- }
172
- } catch {
173
- // Not active
174
- }
175
-
176
- // Check if service has failed
177
- try {
178
- const serviceState = execSync(`systemctl --user is-failed ${serviceName}`, {
179
- encoding: "utf-8",
180
- }).trim();
181
- if (serviceState === "failed") {
182
- state = "failed";
183
- }
184
- } catch {
185
- // Not failed
186
- }
187
-
188
- // Get last run time and result
189
- try {
190
- const props = execSync(
191
- `systemctl --user show ${serviceName} --property=ExecMainStartTimestamp,ExecMainStatus`,
192
- { encoding: "utf-8" }
193
- );
194
- for (const line of props.split("\n")) {
195
- if (line.startsWith("ExecMainStartTimestamp=") && line.trim() !== "ExecMainStartTimestamp=") {
196
- const val = line.split("=", 2)[1].trim();
197
- if (val) lastRun = val;
198
- }
199
- if (line.startsWith("ExecMainStatus=")) {
200
- const val = parseInt(line.split("=", 2)[1].trim(), 10);
201
- if (!isNaN(val)) lastResult = val;
202
- }
203
- }
204
- } catch {
205
- // Couldn't get properties
206
- }
207
-
208
- // Get next run time from timer
209
- try {
210
- const timerProps = execSync(
211
- `systemctl --user show ${timerName} --property=NextElapseUSecRealtime`,
212
- { encoding: "utf-8" }
213
- );
214
- for (const line of timerProps.split("\n")) {
215
- if (line.startsWith("NextElapseUSecRealtime=")) {
216
- const val = line.split("=", 2)[1].trim();
217
- if (val) nextRun = val;
218
- }
219
- }
220
- } catch {
221
- // Couldn't get next run
222
- }
223
-
224
- return { state, lastRun, lastResult, nextRun };
225
- }
226
-
227
- /**
228
- * Run systemctl --user daemon-reload.
229
- */
230
- export function daemonReload(): void {
231
- execSync("systemctl --user daemon-reload", { stdio: "inherit" });
232
- }
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { homedir } from "os";
4
+ import { execSync } from "child_process";
5
+ import type { AgentConfig } from "./types.js";
6
+ import type { ParsedTask } from "./types.js";
7
+
8
+ const UNIT_DIR = path.join(homedir(), ".config", "systemd", "user");
9
+
10
+ function getTimerName(taskId: string): string {
11
+ return `palmier-task-${taskId}.timer`;
12
+ }
13
+
14
+ function getServiceName(taskId: string): string {
15
+ return `palmier-task-${taskId}.service`;
16
+ }
17
+
18
+ /**
19
+ * Convert a cron expression (5-field) to a systemd OnCalendar string.
20
+ * Handles basic cron patterns: minute hour day-of-month month day-of-week
21
+ */
22
+ export function cronToOnCalendar(cron: string): string {
23
+ const parts = cron.trim().split(/\s+/);
24
+ if (parts.length !== 5) {
25
+ throw new Error(`Invalid cron expression (expected 5 fields): ${cron}`);
26
+ }
27
+
28
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
29
+
30
+ // Map cron day-of-week names/numbers to systemd abbreviated names
31
+ const dowMap: Record<string, string> = {
32
+ "0": "Sun",
33
+ "1": "Mon",
34
+ "2": "Tue",
35
+ "3": "Wed",
36
+ "4": "Thu",
37
+ "5": "Fri",
38
+ "6": "Sat",
39
+ "7": "Sun",
40
+ };
41
+
42
+ // Convert day-of-week
43
+ let dow = dayOfWeek === "*" ? "*" : dayOfWeek;
44
+ if (dowMap[dow]) {
45
+ dow = dowMap[dow];
46
+ }
47
+
48
+ // Build OnCalendar string
49
+ // Format: DayOfWeek Year-Month-Day Hour:Minute:Second
50
+ const monthPart = month === "*" ? "*" : month.padStart(2, "0");
51
+ const dayPart = dayOfMonth === "*" ? "*" : dayOfMonth.padStart(2, "0");
52
+ const hourPart = hour === "*" ? "*" : hour.padStart(2, "0");
53
+ const minutePart = minute === "*" ? "*" : minute.padStart(2, "0");
54
+
55
+ if (dow === "*") {
56
+ return `*-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
57
+ }
58
+
59
+ return `${dow} *-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
60
+ }
61
+
62
+ /**
63
+ * Install a systemd user timer + service for a task.
64
+ */
65
+ export function installTaskTimer(config: AgentConfig, task: ParsedTask): void {
66
+ fs.mkdirSync(UNIT_DIR, { recursive: true });
67
+
68
+ const taskId = task.frontmatter.id;
69
+ const serviceName = getServiceName(taskId);
70
+ const timerName = getTimerName(taskId);
71
+
72
+ // Determine the palmier binary path
73
+ const palmierBin = process.argv[1] || "palmier";
74
+
75
+ // Generate service unit
76
+ const serviceContent = `[Unit]
77
+ Description=Palmier Task: ${taskId}
78
+
79
+ [Service]
80
+ Type=oneshot
81
+ ExecStart=${palmierBin} run ${taskId}
82
+ WorkingDirectory=${config.projectRoot}
83
+ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
84
+ `;
85
+
86
+ // Write service unit (always needed for on-demand runs)
87
+ fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
88
+ daemonReload();
89
+
90
+ // Only create and enable a timer if there are actual triggers
91
+ const triggers = task.frontmatter.triggers || [];
92
+ const onCalendarLines: string[] = [];
93
+ for (const trigger of triggers) {
94
+ if (trigger.type === "cron") {
95
+ onCalendarLines.push(`OnCalendar=${cronToOnCalendar(trigger.value)}`);
96
+ } else if (trigger.type === "once") {
97
+ onCalendarLines.push(`OnActiveSec=${trigger.value}`);
98
+ }
99
+ }
100
+
101
+ if (onCalendarLines.length > 0) {
102
+ const timerContent = `[Unit]
103
+ Description=Timer for Palmier Task: ${taskId}
104
+
105
+ [Timer]
106
+ ${onCalendarLines.join("\n")}
107
+ Persistent=true
108
+
109
+ [Install]
110
+ WantedBy=timers.target
111
+ `;
112
+ fs.writeFileSync(path.join(UNIT_DIR, timerName), timerContent, "utf-8");
113
+ daemonReload();
114
+
115
+ try {
116
+ execSync(`systemctl --user enable --now ${timerName}`, { encoding: "utf-8" });
117
+ } catch (err: unknown) {
118
+ const e = err as { stderr?: string };
119
+ console.error(`Failed to enable timer ${timerName}: ${e.stderr || err}`);
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Remove a task's systemd timer and service files.
126
+ */
127
+ export function removeTaskTimer(taskId: string): void {
128
+ const timerName = getTimerName(taskId);
129
+ const serviceName = getServiceName(taskId);
130
+
131
+ const timerPath = path.join(UNIT_DIR, timerName);
132
+ const servicePath = path.join(UNIT_DIR, serviceName);
133
+
134
+ // Only stop/disable the timer if the file exists
135
+ if (fs.existsSync(timerPath)) {
136
+ try {
137
+ execSync(`systemctl --user stop ${timerName}`, { encoding: "utf-8" });
138
+ } catch {
139
+ // Timer might not be running
140
+ }
141
+ try {
142
+ execSync(`systemctl --user disable ${timerName}`, { encoding: "utf-8" });
143
+ } catch {
144
+ // Timer might not be enabled
145
+ }
146
+ fs.unlinkSync(timerPath);
147
+ }
148
+
149
+ if (fs.existsSync(servicePath)) fs.unlinkSync(servicePath);
150
+
151
+ daemonReload();
152
+ }
153
+
154
+ /**
155
+ * Run systemctl --user daemon-reload.
156
+ */
157
+ export function daemonReload(): void {
158
+ try {
159
+ execSync("systemctl --user daemon-reload", { encoding: "utf-8" });
160
+ } catch (err: unknown) {
161
+ const e = err as { stderr?: string };
162
+ console.error(`daemon-reload failed: ${e.stderr || err}`);
163
+ }
164
+ }
package/src/task.ts CHANGED
@@ -35,6 +35,9 @@ function parseTaskContent(content: string): ParsedTask {
35
35
  throw new Error("TASK.md frontmatter must include at least: id");
36
36
  }
37
37
 
38
+ frontmatter.command_line ??= "claude -p --dangerously-skip-permissions";
39
+ frontmatter.triggers_enabled ??= true;
40
+
38
41
  return { frontmatter, body };
39
42
  }
40
43
 
package/src/types.ts CHANGED
@@ -1,63 +1,41 @@
1
- export interface AgentConfig {
2
- agentId: string;
3
- userId: string;
4
- natsUrl: string;
5
- natsWsUrl: string;
6
- natsToken: string;
7
- projectRoot: string;
8
- }
9
-
10
- export interface TaskFrontmatter {
11
- id: string;
12
- name: string;
13
- user_prompt: string;
14
- triggers: Trigger[];
15
- requires_confirmation: boolean;
16
- suppress_permissions: boolean;
17
- enabled: boolean;
18
- }
19
-
20
- export interface Trigger {
21
- type: "cron" | "once";
22
- value: string;
23
- }
24
-
25
- export interface ParsedTask {
26
- frontmatter: TaskFrontmatter;
27
- body: string;
28
- }
29
-
30
- export interface TaskStatus {
31
- state: "active" | "inactive" | "failed";
32
- lastRun?: string;
33
- lastResult?: number;
34
- nextRun?: string;
35
- }
36
-
37
- export interface TaskWithStatus extends ParsedTask {
38
- status: TaskStatus;
39
- }
40
-
41
- export interface HookPayload {
42
- type: "confirm" | "permission" | "input";
43
- task_id: string;
44
- hook_id: string;
45
- agent_id: string;
46
- user_id: string;
47
- details: Record<string, unknown>;
48
- status: string;
49
- }
50
-
51
- export interface ClaudeHookEvent {
52
- hook_name: string;
53
- session_id: string;
54
- tool_name?: string;
55
- tool_input?: Record<string, unknown>;
56
- message?: string;
57
- [key: string]: unknown;
58
- }
59
-
60
- export interface RpcMessage {
61
- method: string;
62
- params: Record<string, unknown>;
63
- }
1
+ export interface AgentConfig {
2
+ agentId: string;
3
+ userId: string;
4
+ natsUrl: string;
5
+ natsWsUrl: string;
6
+ natsToken: string;
7
+ projectRoot: string;
8
+ }
9
+
10
+ export interface TaskFrontmatter {
11
+ id: string;
12
+ user_prompt: string;
13
+ command_line: string;
14
+ triggers: Trigger[];
15
+ triggers_enabled: boolean;
16
+ requires_confirmation: boolean;
17
+ }
18
+
19
+ export interface Trigger {
20
+ type: "cron" | "once";
21
+ value: string;
22
+ }
23
+
24
+ export interface ParsedTask {
25
+ frontmatter: TaskFrontmatter;
26
+ body: string;
27
+ }
28
+
29
+ export interface ConfirmPayload {
30
+ type: "confirm";
31
+ task_id: string;
32
+ agent_id: string;
33
+ user_id: string;
34
+ details: Record<string, unknown>;
35
+ status: "pending" | "confirmed" | "aborted";
36
+ }
37
+
38
+ export interface RpcMessage {
39
+ method: string;
40
+ params: Record<string, unknown>;
41
+ }