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/CLAUDE.md +5 -0
- package/README.md +51 -5
- package/dist/commands/hook.js +32 -5
- package/dist/commands/init.js +16 -29
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +81 -73
- package/dist/commands/serve.js +73 -28
- package/dist/commands/task-generation.md +28 -0
- package/dist/index.js +0 -7
- package/dist/systemd.d.ts +1 -5
- package/dist/systemd.js +54 -114
- package/dist/task.js +2 -0
- package/dist/types.d.ts +5 -24
- package/package.json +33 -35
- package/src/commands/init.ts +121 -141
- package/src/commands/run.ts +205 -197
- package/src/commands/serve.ts +287 -240
- package/src/commands/task-generation.md +28 -0
- package/src/index.ts +0 -8
- package/src/nats-client.ts +15 -15
- package/src/systemd.ts +164 -232
- package/src/task.ts +3 -0
- package/src/types.ts +41 -63
- package/src/commands/hook.ts +0 -240
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
|
|
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: ${
|
|
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
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
triggers: Trigger[];
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|