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/dist/commands/serve.js
CHANGED
|
@@ -1,11 +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";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
4
8
|
import { StringCodec } from "nats";
|
|
5
9
|
import { loadConfig } from "../config.js";
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
function shellEscape(arg) {
|
|
12
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
13
|
+
}
|
|
14
|
+
const TASK_GENERATION_PROMPT = fs.readFileSync(path.join(__dirname, "task-generation.md"), "utf-8");
|
|
6
15
|
import { connectNats } from "../nats-client.js";
|
|
7
16
|
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir } from "../task.js";
|
|
8
|
-
import { installTaskTimer, removeTaskTimer
|
|
17
|
+
import { installTaskTimer, removeTaskTimer } from "../systemd.js";
|
|
9
18
|
/**
|
|
10
19
|
* Start the persistent NATS RPC handler.
|
|
11
20
|
*/
|
|
@@ -25,10 +34,10 @@ export async function serveCommand() {
|
|
|
25
34
|
};
|
|
26
35
|
process.on("SIGINT", shutdown);
|
|
27
36
|
process.on("SIGTERM", shutdown);
|
|
28
|
-
// On startup, clean up orphaned pending-
|
|
37
|
+
// On startup, clean up orphaned pending-confirmation keys for this agent
|
|
29
38
|
try {
|
|
30
39
|
const js = nc.jetstream();
|
|
31
|
-
const kv = await js.views.kv("pending-
|
|
40
|
+
const kv = await js.views.kv("pending-confirmation");
|
|
32
41
|
const keys = await kv.keys();
|
|
33
42
|
for await (const key of keys) {
|
|
34
43
|
if (key.startsWith(`${config.agentId}.`)) {
|
|
@@ -38,7 +47,7 @@ export async function serveCommand() {
|
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
49
|
catch (err) {
|
|
41
|
-
console.error(`Warning: could not clean up pending-
|
|
50
|
+
console.error(`Warning: could not clean up pending-confirmation KV: ${err}`);
|
|
42
51
|
}
|
|
43
52
|
console.log("Agent serving. Waiting for RPC messages...");
|
|
44
53
|
for await (const msg of sub) {
|
|
@@ -72,19 +81,20 @@ export async function serveCommand() {
|
|
|
72
81
|
console.error(`RPC error (${method}):`, err);
|
|
73
82
|
response = { error: String(err) };
|
|
74
83
|
}
|
|
84
|
+
console.log(`RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
|
|
75
85
|
if (msg.reply) {
|
|
76
86
|
msg.respond(sc.encode(JSON.stringify(response)));
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
|
-
function flattenTask(task
|
|
80
|
-
return { ...task.frontmatter, body: task.body
|
|
89
|
+
function flattenTask(task) {
|
|
90
|
+
return { ...task.frontmatter, body: task.body };
|
|
81
91
|
}
|
|
82
92
|
async function handleRpc(request) {
|
|
83
93
|
switch (request.method) {
|
|
84
94
|
case "task.list": {
|
|
85
95
|
const tasks = listTasks(config.projectRoot);
|
|
86
96
|
return {
|
|
87
|
-
tasks: tasks.map((task) => flattenTask(task
|
|
97
|
+
tasks: tasks.map((task) => flattenTask(task)),
|
|
88
98
|
};
|
|
89
99
|
}
|
|
90
100
|
case "task.create": {
|
|
@@ -94,17 +104,18 @@ export async function serveCommand() {
|
|
|
94
104
|
const task = {
|
|
95
105
|
frontmatter: {
|
|
96
106
|
id,
|
|
97
|
-
name: params.name,
|
|
98
107
|
user_prompt: params.user_prompt,
|
|
108
|
+
command_line: params.command_line ?? "claude -p --dangerously-skip-permissions",
|
|
99
109
|
triggers: params.triggers ?? [],
|
|
110
|
+
triggers_enabled: params.triggers_enabled ?? true,
|
|
100
111
|
requires_confirmation: params.requires_confirmation ?? true,
|
|
101
|
-
suppress_permissions: params.suppress_permissions ?? false,
|
|
102
|
-
enabled: params.enabled ?? true,
|
|
103
112
|
},
|
|
104
113
|
body: params.body || "",
|
|
105
114
|
};
|
|
106
115
|
writeTaskFile(taskDir, task);
|
|
107
|
-
|
|
116
|
+
if (task.frontmatter.triggers_enabled) {
|
|
117
|
+
installTaskTimer(config, task);
|
|
118
|
+
}
|
|
108
119
|
return flattenTask(task);
|
|
109
120
|
}
|
|
110
121
|
case "task.update": {
|
|
@@ -112,25 +123,25 @@ export async function serveCommand() {
|
|
|
112
123
|
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
113
124
|
const existing = parseTaskFile(taskDir);
|
|
114
125
|
// Merge updates
|
|
115
|
-
if (params.name !== undefined)
|
|
116
|
-
existing.frontmatter.name = params.name;
|
|
117
126
|
if (params.user_prompt !== undefined)
|
|
118
127
|
existing.frontmatter.user_prompt = params.user_prompt;
|
|
128
|
+
if (params.command_line !== undefined)
|
|
129
|
+
existing.frontmatter.command_line = params.command_line;
|
|
119
130
|
if (params.triggers !== undefined)
|
|
120
131
|
existing.frontmatter.triggers = params.triggers;
|
|
132
|
+
if (params.triggers_enabled !== undefined)
|
|
133
|
+
existing.frontmatter.triggers_enabled = params.triggers_enabled;
|
|
121
134
|
if (params.requires_confirmation !== undefined)
|
|
122
135
|
existing.frontmatter.requires_confirmation = params.requires_confirmation;
|
|
123
|
-
if (params.suppress_permissions !== undefined)
|
|
124
|
-
existing.frontmatter.suppress_permissions = params.suppress_permissions;
|
|
125
|
-
if (params.enabled !== undefined)
|
|
126
|
-
existing.frontmatter.enabled = params.enabled;
|
|
127
136
|
if (params.body !== undefined)
|
|
128
137
|
existing.body = params.body;
|
|
129
138
|
writeTaskFile(taskDir, existing);
|
|
130
|
-
// Reinstall
|
|
139
|
+
// Reinstall or remove timers based on triggers_enabled
|
|
131
140
|
removeTaskTimer(params.id);
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
if (existing.frontmatter.triggers_enabled) {
|
|
142
|
+
installTaskTimer(config, existing);
|
|
143
|
+
}
|
|
144
|
+
return flattenTask(existing);
|
|
134
145
|
}
|
|
135
146
|
case "task.delete": {
|
|
136
147
|
const params = request.params;
|
|
@@ -144,8 +155,10 @@ export async function serveCommand() {
|
|
|
144
155
|
}
|
|
145
156
|
case "task.generate": {
|
|
146
157
|
const params = request.params;
|
|
158
|
+
const commandLine = params.command_line || "claude -p --dangerously-skip-permissions";
|
|
159
|
+
const fullPrompt = TASK_GENERATION_PROMPT + params.prompt;
|
|
147
160
|
try {
|
|
148
|
-
const output = execSync(
|
|
161
|
+
const output = execSync(`${commandLine} ${shellEscape(fullPrompt)}`, {
|
|
149
162
|
encoding: "utf-8",
|
|
150
163
|
cwd: config.projectRoot,
|
|
151
164
|
timeout: 120_000,
|
|
@@ -154,24 +167,34 @@ export async function serveCommand() {
|
|
|
154
167
|
}
|
|
155
168
|
catch (err) {
|
|
156
169
|
const error = err;
|
|
157
|
-
return { error: "
|
|
170
|
+
return { error: "generation command failed", stdout: error.stdout, stderr: error.stderr };
|
|
158
171
|
}
|
|
159
172
|
}
|
|
160
173
|
case "task.run": {
|
|
161
174
|
const params = request.params;
|
|
162
175
|
const serviceName = `palmier-task-${params.id}.service`;
|
|
163
176
|
try {
|
|
164
|
-
|
|
177
|
+
await execAsync(`systemctl --user start --no-block ${serviceName}`);
|
|
165
178
|
return { ok: true, task_id: params.id };
|
|
166
179
|
}
|
|
167
180
|
catch (err) {
|
|
168
|
-
|
|
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}` };
|
|
169
184
|
}
|
|
170
185
|
}
|
|
171
|
-
case "task.
|
|
186
|
+
case "task.abort": {
|
|
172
187
|
const params = request.params;
|
|
173
|
-
const
|
|
174
|
-
|
|
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
|
+
}
|
|
175
198
|
}
|
|
176
199
|
case "task.logs": {
|
|
177
200
|
const params = request.params;
|
|
@@ -185,6 +208,28 @@ export async function serveCommand() {
|
|
|
185
208
|
return { task_id: params.id, logs: error.stdout || "", error: error.stderr };
|
|
186
209
|
}
|
|
187
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
|
+
}
|
|
188
233
|
default:
|
|
189
234
|
return { error: `Unknown method: ${request.method}` };
|
|
190
235
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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",
|
|
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
|
+
}
|