palmier 0.1.9 → 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 +68 -29
- package/dist/commands/task-generation.md +28 -21
- 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 -250
- package/src/commands/task-generation.md +28 -21
- 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/dist/commands/serve.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
-
import { execSync } from "child_process";
|
|
2
|
+
import { execSync, exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
const execAsync = promisify(exec);
|
|
3
5
|
import * as fs from "fs";
|
|
4
6
|
import * as path from "path";
|
|
5
7
|
import { fileURLToPath } from "url";
|
|
6
8
|
import { StringCodec } from "nats";
|
|
7
9
|
import { loadConfig } from "../config.js";
|
|
8
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
function shellEscape(arg) {
|
|
12
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
13
|
+
}
|
|
9
14
|
const TASK_GENERATION_PROMPT = fs.readFileSync(path.join(__dirname, "task-generation.md"), "utf-8");
|
|
10
15
|
import { connectNats } from "../nats-client.js";
|
|
11
16
|
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir } from "../task.js";
|
|
12
|
-
import { installTaskTimer, removeTaskTimer
|
|
17
|
+
import { installTaskTimer, removeTaskTimer } from "../systemd.js";
|
|
13
18
|
/**
|
|
14
19
|
* Start the persistent NATS RPC handler.
|
|
15
20
|
*/
|
|
@@ -29,10 +34,10 @@ export async function serveCommand() {
|
|
|
29
34
|
};
|
|
30
35
|
process.on("SIGINT", shutdown);
|
|
31
36
|
process.on("SIGTERM", shutdown);
|
|
32
|
-
// On startup, clean up orphaned pending-
|
|
37
|
+
// On startup, clean up orphaned pending-confirmation keys for this agent
|
|
33
38
|
try {
|
|
34
39
|
const js = nc.jetstream();
|
|
35
|
-
const kv = await js.views.kv("pending-
|
|
40
|
+
const kv = await js.views.kv("pending-confirmation");
|
|
36
41
|
const keys = await kv.keys();
|
|
37
42
|
for await (const key of keys) {
|
|
38
43
|
if (key.startsWith(`${config.agentId}.`)) {
|
|
@@ -42,7 +47,7 @@ export async function serveCommand() {
|
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
catch (err) {
|
|
45
|
-
console.error(`Warning: could not clean up pending-
|
|
50
|
+
console.error(`Warning: could not clean up pending-confirmation KV: ${err}`);
|
|
46
51
|
}
|
|
47
52
|
console.log("Agent serving. Waiting for RPC messages...");
|
|
48
53
|
for await (const msg of sub) {
|
|
@@ -76,19 +81,20 @@ export async function serveCommand() {
|
|
|
76
81
|
console.error(`RPC error (${method}):`, err);
|
|
77
82
|
response = { error: String(err) };
|
|
78
83
|
}
|
|
84
|
+
console.log(`RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
|
|
79
85
|
if (msg.reply) {
|
|
80
86
|
msg.respond(sc.encode(JSON.stringify(response)));
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
|
-
function flattenTask(task
|
|
84
|
-
return { ...task.frontmatter, body: task.body
|
|
89
|
+
function flattenTask(task) {
|
|
90
|
+
return { ...task.frontmatter, body: task.body };
|
|
85
91
|
}
|
|
86
92
|
async function handleRpc(request) {
|
|
87
93
|
switch (request.method) {
|
|
88
94
|
case "task.list": {
|
|
89
95
|
const tasks = listTasks(config.projectRoot);
|
|
90
96
|
return {
|
|
91
|
-
tasks: tasks.map((task) => flattenTask(task
|
|
97
|
+
tasks: tasks.map((task) => flattenTask(task)),
|
|
92
98
|
};
|
|
93
99
|
}
|
|
94
100
|
case "task.create": {
|
|
@@ -98,17 +104,18 @@ export async function serveCommand() {
|
|
|
98
104
|
const task = {
|
|
99
105
|
frontmatter: {
|
|
100
106
|
id,
|
|
101
|
-
name: params.name,
|
|
102
107
|
user_prompt: params.user_prompt,
|
|
108
|
+
command_line: params.command_line ?? "claude -p --dangerously-skip-permissions",
|
|
103
109
|
triggers: params.triggers ?? [],
|
|
110
|
+
triggers_enabled: params.triggers_enabled ?? true,
|
|
104
111
|
requires_confirmation: params.requires_confirmation ?? true,
|
|
105
|
-
suppress_permissions: params.suppress_permissions ?? false,
|
|
106
|
-
enabled: params.enabled ?? true,
|
|
107
112
|
},
|
|
108
113
|
body: params.body || "",
|
|
109
114
|
};
|
|
110
115
|
writeTaskFile(taskDir, task);
|
|
111
|
-
|
|
116
|
+
if (task.frontmatter.triggers_enabled) {
|
|
117
|
+
installTaskTimer(config, task);
|
|
118
|
+
}
|
|
112
119
|
return flattenTask(task);
|
|
113
120
|
}
|
|
114
121
|
case "task.update": {
|
|
@@ -116,25 +123,25 @@ export async function serveCommand() {
|
|
|
116
123
|
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
117
124
|
const existing = parseTaskFile(taskDir);
|
|
118
125
|
// Merge updates
|
|
119
|
-
if (params.name !== undefined)
|
|
120
|
-
existing.frontmatter.name = params.name;
|
|
121
126
|
if (params.user_prompt !== undefined)
|
|
122
127
|
existing.frontmatter.user_prompt = params.user_prompt;
|
|
128
|
+
if (params.command_line !== undefined)
|
|
129
|
+
existing.frontmatter.command_line = params.command_line;
|
|
123
130
|
if (params.triggers !== undefined)
|
|
124
131
|
existing.frontmatter.triggers = params.triggers;
|
|
132
|
+
if (params.triggers_enabled !== undefined)
|
|
133
|
+
existing.frontmatter.triggers_enabled = params.triggers_enabled;
|
|
125
134
|
if (params.requires_confirmation !== undefined)
|
|
126
135
|
existing.frontmatter.requires_confirmation = params.requires_confirmation;
|
|
127
|
-
if (params.suppress_permissions !== undefined)
|
|
128
|
-
existing.frontmatter.suppress_permissions = params.suppress_permissions;
|
|
129
|
-
if (params.enabled !== undefined)
|
|
130
|
-
existing.frontmatter.enabled = params.enabled;
|
|
131
136
|
if (params.body !== undefined)
|
|
132
137
|
existing.body = params.body;
|
|
133
138
|
writeTaskFile(taskDir, existing);
|
|
134
|
-
// Reinstall
|
|
139
|
+
// Reinstall or remove timers based on triggers_enabled
|
|
135
140
|
removeTaskTimer(params.id);
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
if (existing.frontmatter.triggers_enabled) {
|
|
142
|
+
installTaskTimer(config, existing);
|
|
143
|
+
}
|
|
144
|
+
return flattenTask(existing);
|
|
138
145
|
}
|
|
139
146
|
case "task.delete": {
|
|
140
147
|
const params = request.params;
|
|
@@ -148,10 +155,10 @@ export async function serveCommand() {
|
|
|
148
155
|
}
|
|
149
156
|
case "task.generate": {
|
|
150
157
|
const params = request.params;
|
|
158
|
+
const commandLine = params.command_line || "claude -p --dangerously-skip-permissions";
|
|
151
159
|
const fullPrompt = TASK_GENERATION_PROMPT + params.prompt;
|
|
152
160
|
try {
|
|
153
|
-
const output = execSync(
|
|
154
|
-
input: fullPrompt,
|
|
161
|
+
const output = execSync(`${commandLine} ${shellEscape(fullPrompt)}`, {
|
|
155
162
|
encoding: "utf-8",
|
|
156
163
|
cwd: config.projectRoot,
|
|
157
164
|
timeout: 120_000,
|
|
@@ -160,24 +167,34 @@ export async function serveCommand() {
|
|
|
160
167
|
}
|
|
161
168
|
catch (err) {
|
|
162
169
|
const error = err;
|
|
163
|
-
return { error: "
|
|
170
|
+
return { error: "generation command failed", stdout: error.stdout, stderr: error.stderr };
|
|
164
171
|
}
|
|
165
172
|
}
|
|
166
173
|
case "task.run": {
|
|
167
174
|
const params = request.params;
|
|
168
175
|
const serviceName = `palmier-task-${params.id}.service`;
|
|
169
176
|
try {
|
|
170
|
-
|
|
177
|
+
await execAsync(`systemctl --user start --no-block ${serviceName}`);
|
|
171
178
|
return { ok: true, task_id: params.id };
|
|
172
179
|
}
|
|
173
180
|
catch (err) {
|
|
174
|
-
|
|
181
|
+
const e = err;
|
|
182
|
+
console.error(`task.run failed for ${params.id}: ${e.stderr || e.message}`);
|
|
183
|
+
return { error: `Failed to start task: ${e.stderr || e.message}` };
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
|
-
case "task.
|
|
186
|
+
case "task.abort": {
|
|
178
187
|
const params = request.params;
|
|
179
|
-
const
|
|
180
|
-
|
|
188
|
+
const serviceName = `palmier-task-${params.id}.service`;
|
|
189
|
+
try {
|
|
190
|
+
await execAsync(`systemctl --user stop ${serviceName}`);
|
|
191
|
+
return { ok: true, task_id: params.id };
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
const e = err;
|
|
195
|
+
console.error(`task.abort failed for ${params.id}: ${e.stderr || e.message}`);
|
|
196
|
+
return { error: `Failed to abort task: ${e.stderr || e.message}` };
|
|
197
|
+
}
|
|
181
198
|
}
|
|
182
199
|
case "task.logs": {
|
|
183
200
|
const params = request.params;
|
|
@@ -191,6 +208,28 @@ export async function serveCommand() {
|
|
|
191
208
|
return { task_id: params.id, logs: error.stdout || "", error: error.stderr };
|
|
192
209
|
}
|
|
193
210
|
}
|
|
211
|
+
case "task.result": {
|
|
212
|
+
const params = request.params;
|
|
213
|
+
const resultPath = path.join(config.projectRoot, "tasks", params.id, "RESULT.md");
|
|
214
|
+
try {
|
|
215
|
+
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
216
|
+
// Parse optional frontmatter
|
|
217
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
218
|
+
if (fmMatch) {
|
|
219
|
+
const meta = {};
|
|
220
|
+
for (const line of fmMatch[1].split("\n")) {
|
|
221
|
+
const [key, val] = line.split(": ");
|
|
222
|
+
if (key && val)
|
|
223
|
+
meta[key.trim()] = Number(val.trim());
|
|
224
|
+
}
|
|
225
|
+
return { task_id: params.id, content: fmMatch[2], start_time: meta.start_time, end_time: meta.end_time };
|
|
226
|
+
}
|
|
227
|
+
return { task_id: params.id, content: raw };
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return { task_id: params.id, error: "No result file found" };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
194
233
|
default:
|
|
195
234
|
return { error: `Unknown method: ${request.method}` };
|
|
196
235
|
}
|
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
You are a
|
|
2
|
-
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
### 1.
|
|
6
|
-
What the task accomplishes
|
|
7
|
-
|
|
8
|
-
### 2.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
You are a planning agent for a personal computer AI agent. Given a task description, produce a detailed Markdown execution plan that the agent can later follow step by step. **Do not execute any part of the task yourself.**
|
|
2
|
+
|
|
3
|
+
The plan must include the following sections:
|
|
4
|
+
|
|
5
|
+
### 1. Goal
|
|
6
|
+
What the task accomplishes and the expected end state.
|
|
7
|
+
|
|
8
|
+
### 2. Prerequisites
|
|
9
|
+
What must be true before starting:
|
|
10
|
+
- Required software and versions
|
|
11
|
+
- Files or data that must be present
|
|
12
|
+
- Permissions or access needed
|
|
13
|
+
- Environment state (e.g., running services, network access)
|
|
14
|
+
|
|
15
|
+
### 3. Plan
|
|
16
|
+
A numbered sequence of concrete, actionable steps to complete the task.
|
|
17
|
+
Use sub-steps for complex actions. Include conditional branches where behavior may vary (e.g., "If file exists, do A; otherwise, do B"). Each step should be specific enough that the agent can execute it without ambiguity.
|
|
18
|
+
|
|
19
|
+
### 4. Edge Cases & Risks
|
|
20
|
+
Anything that could go wrong and how to handle it:
|
|
21
|
+
- Common failure modes
|
|
22
|
+
- Platform-specific differences
|
|
23
|
+
- Race conditions or timing issues
|
|
24
|
+
- Data loss risks and mitigation
|
|
25
|
+
|
|
26
|
+
Format the entire document as Markdown with proper headings, code blocks for commands, and tables where appropriate.
|
|
27
|
+
|
|
28
|
+
**Task description:**
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,6 @@ import { dirname, join } from "path";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { initCommand } from "./commands/init.js";
|
|
8
8
|
import { runCommand } from "./commands/run.js";
|
|
9
|
-
import { hookCommand } from "./commands/hook.js";
|
|
10
9
|
import { serveCommand } from "./commands/serve.js";
|
|
11
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
11
|
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
@@ -28,12 +27,6 @@ program
|
|
|
28
27
|
.action(async (taskId) => {
|
|
29
28
|
await runCommand(taskId);
|
|
30
29
|
});
|
|
31
|
-
program
|
|
32
|
-
.command("hook")
|
|
33
|
-
.description("Handle a Claude Code hook event (reads from stdin)")
|
|
34
|
-
.action(async () => {
|
|
35
|
-
await hookCommand();
|
|
36
|
-
});
|
|
37
30
|
program
|
|
38
31
|
.command("serve", { isDefault: true })
|
|
39
32
|
.description("Start the persistent NATS RPC handler")
|
package/dist/systemd.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentConfig } from "./types.js";
|
|
2
|
-
import type { ParsedTask
|
|
2
|
+
import type { ParsedTask } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Convert a cron expression (5-field) to a systemd OnCalendar string.
|
|
5
5
|
* Handles basic cron patterns: minute hour day-of-month month day-of-week
|
|
@@ -13,10 +13,6 @@ export declare function installTaskTimer(config: AgentConfig, task: ParsedTask):
|
|
|
13
13
|
* Remove a task's systemd timer and service files.
|
|
14
14
|
*/
|
|
15
15
|
export declare function removeTaskTimer(taskId: string): void;
|
|
16
|
-
/**
|
|
17
|
-
* Query systemd for task status information.
|
|
18
|
-
*/
|
|
19
|
-
export declare function getTaskStatus(taskId: string): TaskStatus;
|
|
20
16
|
/**
|
|
21
17
|
* Run systemctl --user daemon-reload.
|
|
22
18
|
*/
|
package/dist/systemd.js
CHANGED
|
@@ -57,46 +57,49 @@ export function installTaskTimer(config, task) {
|
|
|
57
57
|
// Determine the palmier binary path
|
|
58
58
|
const palmierBin = process.argv[1] || "palmier";
|
|
59
59
|
// Generate service unit
|
|
60
|
-
const serviceContent = `[Unit]
|
|
61
|
-
Description=Palmier Task: ${
|
|
62
|
-
|
|
63
|
-
[Service]
|
|
64
|
-
Type=oneshot
|
|
65
|
-
ExecStart=${palmierBin} run ${taskId}
|
|
66
|
-
WorkingDirectory=${config.projectRoot}
|
|
67
|
-
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
60
|
+
const serviceContent = `[Unit]
|
|
61
|
+
Description=Palmier Task: ${taskId}
|
|
62
|
+
|
|
63
|
+
[Service]
|
|
64
|
+
Type=oneshot
|
|
65
|
+
ExecStart=${palmierBin} run ${taskId}
|
|
66
|
+
WorkingDirectory=${config.projectRoot}
|
|
67
|
+
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
68
68
|
`;
|
|
69
|
-
//
|
|
69
|
+
// Write service unit (always needed for on-demand runs)
|
|
70
|
+
fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
|
|
71
|
+
daemonReload();
|
|
72
|
+
// Only create and enable a timer if there are actual triggers
|
|
73
|
+
const triggers = task.frontmatter.triggers || [];
|
|
70
74
|
const onCalendarLines = [];
|
|
71
|
-
for (const trigger of
|
|
75
|
+
for (const trigger of triggers) {
|
|
72
76
|
if (trigger.type === "cron") {
|
|
73
77
|
onCalendarLines.push(`OnCalendar=${cronToOnCalendar(trigger.value)}`);
|
|
74
78
|
}
|
|
75
79
|
else if (trigger.type === "once") {
|
|
76
|
-
// "once" triggers use OnActiveSec or a specific timestamp
|
|
77
80
|
onCalendarLines.push(`OnActiveSec=${trigger.value}`);
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
if (onCalendarLines.length > 0) {
|
|
84
|
+
const timerContent = `[Unit]
|
|
85
|
+
Description=Timer for Palmier Task: ${taskId}
|
|
86
|
+
|
|
87
|
+
[Timer]
|
|
88
|
+
${onCalendarLines.join("\n")}
|
|
89
|
+
Persistent=true
|
|
90
|
+
|
|
91
|
+
[Install]
|
|
92
|
+
WantedBy=timers.target
|
|
89
93
|
`;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
execSync(`systemctl --user enable ${timerName}`, { stdio: "inherit" });
|
|
94
|
+
fs.writeFileSync(path.join(UNIT_DIR, timerName), timerContent, "utf-8");
|
|
95
|
+
daemonReload();
|
|
96
|
+
try {
|
|
97
|
+
execSync(`systemctl --user enable --now ${timerName}`, { encoding: "utf-8" });
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
const e = err;
|
|
101
|
+
console.error(`Failed to enable timer ${timerName}: ${e.stderr || err}`);
|
|
102
|
+
}
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
/**
|
|
@@ -105,101 +108,38 @@ WantedBy=timers.target
|
|
|
105
108
|
export function removeTaskTimer(taskId) {
|
|
106
109
|
const timerName = getTimerName(taskId);
|
|
107
110
|
const serviceName = getServiceName(taskId);
|
|
108
|
-
// Stop and disable
|
|
109
|
-
try {
|
|
110
|
-
execSync(`systemctl --user stop ${timerName}`, { stdio: "inherit" });
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
// Timer might not be running
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
execSync(`systemctl --user disable ${timerName}`, { stdio: "inherit" });
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// Timer might not be enabled
|
|
120
|
-
}
|
|
121
|
-
// Remove unit files
|
|
122
111
|
const timerPath = path.join(UNIT_DIR, timerName);
|
|
123
112
|
const servicePath = path.join(UNIT_DIR, serviceName);
|
|
124
|
-
if
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
daemonReload();
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Query systemd for task status information.
|
|
132
|
-
*/
|
|
133
|
-
export function getTaskStatus(taskId) {
|
|
134
|
-
const timerName = getTimerName(taskId);
|
|
135
|
-
const serviceName = getServiceName(taskId);
|
|
136
|
-
let state = "inactive";
|
|
137
|
-
let lastRun;
|
|
138
|
-
let lastResult;
|
|
139
|
-
let nextRun;
|
|
140
|
-
// Check if timer is active
|
|
141
|
-
try {
|
|
142
|
-
const activeState = execSync(`systemctl --user is-active ${timerName}`, {
|
|
143
|
-
encoding: "utf-8",
|
|
144
|
-
}).trim();
|
|
145
|
-
if (activeState === "active") {
|
|
146
|
-
state = "active";
|
|
113
|
+
// Only stop/disable the timer if the file exists
|
|
114
|
+
if (fs.existsSync(timerPath)) {
|
|
115
|
+
try {
|
|
116
|
+
execSync(`systemctl --user stop ${timerName}`, { encoding: "utf-8" });
|
|
147
117
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// Not active
|
|
151
|
-
}
|
|
152
|
-
// Check if service has failed
|
|
153
|
-
try {
|
|
154
|
-
const serviceState = execSync(`systemctl --user is-failed ${serviceName}`, {
|
|
155
|
-
encoding: "utf-8",
|
|
156
|
-
}).trim();
|
|
157
|
-
if (serviceState === "failed") {
|
|
158
|
-
state = "failed";
|
|
118
|
+
catch {
|
|
119
|
+
// Timer might not be running
|
|
159
120
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// Not failed
|
|
163
|
-
}
|
|
164
|
-
// Get last run time and result
|
|
165
|
-
try {
|
|
166
|
-
const props = execSync(`systemctl --user show ${serviceName} --property=ExecMainStartTimestamp,ExecMainStatus`, { encoding: "utf-8" });
|
|
167
|
-
for (const line of props.split("\n")) {
|
|
168
|
-
if (line.startsWith("ExecMainStartTimestamp=") && line.trim() !== "ExecMainStartTimestamp=") {
|
|
169
|
-
const val = line.split("=", 2)[1].trim();
|
|
170
|
-
if (val)
|
|
171
|
-
lastRun = val;
|
|
172
|
-
}
|
|
173
|
-
if (line.startsWith("ExecMainStatus=")) {
|
|
174
|
-
const val = parseInt(line.split("=", 2)[1].trim(), 10);
|
|
175
|
-
if (!isNaN(val))
|
|
176
|
-
lastResult = val;
|
|
177
|
-
}
|
|
121
|
+
try {
|
|
122
|
+
execSync(`systemctl --user disable ${timerName}`, { encoding: "utf-8" });
|
|
178
123
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// Couldn't get properties
|
|
182
|
-
}
|
|
183
|
-
// Get next run time from timer
|
|
184
|
-
try {
|
|
185
|
-
const timerProps = execSync(`systemctl --user show ${timerName} --property=NextElapseUSecRealtime`, { encoding: "utf-8" });
|
|
186
|
-
for (const line of timerProps.split("\n")) {
|
|
187
|
-
if (line.startsWith("NextElapseUSecRealtime=")) {
|
|
188
|
-
const val = line.split("=", 2)[1].trim();
|
|
189
|
-
if (val)
|
|
190
|
-
nextRun = val;
|
|
191
|
-
}
|
|
124
|
+
catch {
|
|
125
|
+
// Timer might not be enabled
|
|
192
126
|
}
|
|
127
|
+
fs.unlinkSync(timerPath);
|
|
193
128
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return { state, lastRun, lastResult, nextRun };
|
|
129
|
+
if (fs.existsSync(servicePath))
|
|
130
|
+
fs.unlinkSync(servicePath);
|
|
131
|
+
daemonReload();
|
|
198
132
|
}
|
|
199
133
|
/**
|
|
200
134
|
* Run systemctl --user daemon-reload.
|
|
201
135
|
*/
|
|
202
136
|
export function daemonReload() {
|
|
203
|
-
|
|
137
|
+
try {
|
|
138
|
+
execSync("systemctl --user daemon-reload", { encoding: "utf-8" });
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const e = err;
|
|
142
|
+
console.error(`daemon-reload failed: ${e.stderr || err}`);
|
|
143
|
+
}
|
|
204
144
|
}
|
|
205
145
|
//# sourceMappingURL=systemd.js.map
|
package/dist/task.js
CHANGED
|
@@ -26,6 +26,8 @@ function parseTaskContent(content) {
|
|
|
26
26
|
if (!frontmatter.id) {
|
|
27
27
|
throw new Error("TASK.md frontmatter must include at least: id");
|
|
28
28
|
}
|
|
29
|
+
frontmatter.command_line ??= "claude -p --dangerously-skip-permissions";
|
|
30
|
+
frontmatter.triggers_enabled ??= true;
|
|
29
31
|
return { frontmatter, body };
|
|
30
32
|
}
|
|
31
33
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -8,12 +8,11 @@ export interface AgentConfig {
|
|
|
8
8
|
}
|
|
9
9
|
export interface TaskFrontmatter {
|
|
10
10
|
id: string;
|
|
11
|
-
name: string;
|
|
12
11
|
user_prompt: string;
|
|
12
|
+
command_line: string;
|
|
13
13
|
triggers: Trigger[];
|
|
14
|
+
triggers_enabled: boolean;
|
|
14
15
|
requires_confirmation: boolean;
|
|
15
|
-
suppress_permissions: boolean;
|
|
16
|
-
enabled: boolean;
|
|
17
16
|
}
|
|
18
17
|
export interface Trigger {
|
|
19
18
|
type: "cron" | "once";
|
|
@@ -23,31 +22,13 @@ export interface ParsedTask {
|
|
|
23
22
|
frontmatter: TaskFrontmatter;
|
|
24
23
|
body: string;
|
|
25
24
|
}
|
|
26
|
-
export interface
|
|
27
|
-
|
|
28
|
-
lastRun?: string;
|
|
29
|
-
lastResult?: number;
|
|
30
|
-
nextRun?: string;
|
|
31
|
-
}
|
|
32
|
-
export interface TaskWithStatus extends ParsedTask {
|
|
33
|
-
status: TaskStatus;
|
|
34
|
-
}
|
|
35
|
-
export interface HookPayload {
|
|
36
|
-
type: "confirm" | "permission" | "input";
|
|
25
|
+
export interface ConfirmPayload {
|
|
26
|
+
type: "confirm";
|
|
37
27
|
task_id: string;
|
|
38
|
-
hook_id: string;
|
|
39
28
|
agent_id: string;
|
|
40
29
|
user_id: string;
|
|
41
30
|
details: Record<string, unknown>;
|
|
42
|
-
status:
|
|
43
|
-
}
|
|
44
|
-
export interface ClaudeHookEvent {
|
|
45
|
-
hook_name: string;
|
|
46
|
-
session_id: string;
|
|
47
|
-
tool_name?: string;
|
|
48
|
-
tool_input?: Record<string, unknown>;
|
|
49
|
-
message?: string;
|
|
50
|
-
[key: string]: unknown;
|
|
31
|
+
status: "pending" | "confirmed" | "aborted";
|
|
51
32
|
}
|
|
52
33
|
export interface RpcMessage {
|
|
53
34
|
method: string;
|
package/package.json
CHANGED
|
@@ -1,35 +1,33 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "palmier",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Palmier agent CLI - provisions, executes tasks, and serves NATS RPC",
|
|
5
|
-
"license": "ISC",
|
|
6
|
-
"author": "Hongxu Cai",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"main": "dist/index.js",
|
|
9
|
-
"types": "./dist/index.d.ts",
|
|
10
|
-
"bin": {
|
|
11
|
-
"palmier": "dist/index.js"
|
|
12
|
-
},
|
|
13
|
-
"scripts": {
|
|
14
|
-
"dev": "tsx src/index.ts",
|
|
15
|
-
"build": "tsc && node -e \"require('fs').cpSync('src/commands/task-generation.md','dist/commands/task-generation.md')\"",
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "palmier",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Palmier agent CLI - provisions, executes tasks, and serves NATS RPC",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "Hongxu Cai",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"bin": {
|
|
11
|
+
"palmier": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"build": "tsc && node -e \"require('fs').cpSync('src/commands/task-generation.md','dist/commands/task-generation.md')\"",
|
|
16
|
+
"prepare": "npm run build",
|
|
17
|
+
"start": "node dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"commander": "^13.1.0",
|
|
21
|
+
"dotenv": "^16.4.7",
|
|
22
|
+
"nats": "^2.29.1",
|
|
23
|
+
"yaml": "^2.7.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.13.0",
|
|
27
|
+
"tsx": "^4.19.0",
|
|
28
|
+
"typescript": "^5.7.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|