palmier 0.2.7 → 0.2.8
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/.github/workflows/publish.yml +24 -0
- package/CLAUDE.md +9 -9
- package/README.md +288 -286
- package/dist/agents/shared-prompt.js +16 -16
- package/package.json +44 -36
- package/src/agents/claude.ts +44 -44
- package/src/agents/shared-prompt.ts +28 -28
- package/src/commands/run.ts +619 -619
- package/src/nats-client.ts +15 -15
- package/src/rpc-handler.ts +388 -388
- package/src/types.ts +62 -62
- package/dist/commands/hook.d.ts +0 -7
- package/dist/commands/hook.js +0 -208
- package/dist/commands/task-cleanup.d.ts +0 -14
- package/dist/commands/task-cleanup.js +0 -84
- package/dist/commands/task-generation.md +0 -28
- package/dist/systemd.d.ts +0 -20
- package/dist/systemd.js +0 -145
package/src/types.ts
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
export interface HostConfig {
|
|
2
|
-
hostId: string;
|
|
3
|
-
projectRoot: string;
|
|
4
|
-
|
|
5
|
-
// NATS (always enabled)
|
|
6
|
-
nats?: boolean;
|
|
7
|
-
natsUrl?: string;
|
|
8
|
-
natsWsUrl?: string;
|
|
9
|
-
natsToken?: string;
|
|
10
|
-
|
|
11
|
-
// Detected agent CLIs
|
|
12
|
-
agents?: Array<{ key: string; label: string }>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface TaskFrontmatter {
|
|
16
|
-
id: string;
|
|
17
|
-
name: string;
|
|
18
|
-
user_prompt: string;
|
|
19
|
-
agent: string;
|
|
20
|
-
triggers: Trigger[];
|
|
21
|
-
triggers_enabled: boolean;
|
|
22
|
-
requires_confirmation: boolean;
|
|
23
|
-
permissions?: RequiredPermission[];
|
|
24
|
-
command?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface Trigger {
|
|
28
|
-
type: "cron" | "once";
|
|
29
|
-
value: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ParsedTask {
|
|
33
|
-
frontmatter: TaskFrontmatter;
|
|
34
|
-
body: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
|
|
38
|
-
|
|
39
|
-
export interface TaskStatus {
|
|
40
|
-
running_state: TaskRunningState;
|
|
41
|
-
time_stamp: number;
|
|
42
|
-
pending_confirmation?: boolean;
|
|
43
|
-
pending_permission?: RequiredPermission[];
|
|
44
|
-
pending_input?: string[];
|
|
45
|
-
user_input?: string[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface HistoryEntry {
|
|
49
|
-
task_id: string;
|
|
50
|
-
result_file: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface RequiredPermission {
|
|
54
|
-
name: string;
|
|
55
|
-
description: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface RpcMessage {
|
|
59
|
-
method: string;
|
|
60
|
-
params: Record<string, unknown>;
|
|
61
|
-
sessionToken?: string;
|
|
62
|
-
}
|
|
1
|
+
export interface HostConfig {
|
|
2
|
+
hostId: string;
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
|
|
5
|
+
// NATS (always enabled)
|
|
6
|
+
nats?: boolean;
|
|
7
|
+
natsUrl?: string;
|
|
8
|
+
natsWsUrl?: string;
|
|
9
|
+
natsToken?: string;
|
|
10
|
+
|
|
11
|
+
// Detected agent CLIs
|
|
12
|
+
agents?: Array<{ key: string; label: string }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TaskFrontmatter {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
user_prompt: string;
|
|
19
|
+
agent: string;
|
|
20
|
+
triggers: Trigger[];
|
|
21
|
+
triggers_enabled: boolean;
|
|
22
|
+
requires_confirmation: boolean;
|
|
23
|
+
permissions?: RequiredPermission[];
|
|
24
|
+
command?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Trigger {
|
|
28
|
+
type: "cron" | "once";
|
|
29
|
+
value: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ParsedTask {
|
|
33
|
+
frontmatter: TaskFrontmatter;
|
|
34
|
+
body: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
|
|
38
|
+
|
|
39
|
+
export interface TaskStatus {
|
|
40
|
+
running_state: TaskRunningState;
|
|
41
|
+
time_stamp: number;
|
|
42
|
+
pending_confirmation?: boolean;
|
|
43
|
+
pending_permission?: RequiredPermission[];
|
|
44
|
+
pending_input?: string[];
|
|
45
|
+
user_input?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface HistoryEntry {
|
|
49
|
+
task_id: string;
|
|
50
|
+
result_file: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RequiredPermission {
|
|
54
|
+
name: string;
|
|
55
|
+
description: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface RpcMessage {
|
|
59
|
+
method: string;
|
|
60
|
+
params: Record<string, unknown>;
|
|
61
|
+
sessionToken?: string;
|
|
62
|
+
}
|
package/dist/commands/hook.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handle a Claude Code hook invocation.
|
|
3
|
-
* Called by Claude Code as a subprocess. Reads hook event from stdin,
|
|
4
|
-
* dispatches by hook_name, and outputs response to stdout.
|
|
5
|
-
*/
|
|
6
|
-
export declare function hookCommand(): Promise<void>;
|
|
7
|
-
//# sourceMappingURL=hook.d.ts.map
|
package/dist/commands/hook.js
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from "uuid";
|
|
2
|
-
import { StringCodec } from "nats";
|
|
3
|
-
import { appendFileSync } from "fs";
|
|
4
|
-
import { loadConfig } from "../config.js";
|
|
5
|
-
import { connectNats } from "../nats-client.js";
|
|
6
|
-
function log(msg) {
|
|
7
|
-
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
8
|
-
appendFileSync("/tmp/palmier-hook.log", line);
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Handle a Claude Code hook invocation.
|
|
12
|
-
* Called by Claude Code as a subprocess. Reads hook event from stdin,
|
|
13
|
-
* dispatches by hook_name, and outputs response to stdout.
|
|
14
|
-
*/
|
|
15
|
-
export async function hookCommand() {
|
|
16
|
-
const rawInput = await readStdin();
|
|
17
|
-
let event;
|
|
18
|
-
try {
|
|
19
|
-
event = JSON.parse(rawInput);
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
console.error("Failed to parse hook event from stdin");
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
log(`received: ${JSON.stringify(event).slice(0, 500)}`);
|
|
26
|
-
const taskId = process.env.PALMIER_TASK_ID;
|
|
27
|
-
if (!taskId) {
|
|
28
|
-
log("no PALMIER_TASK_ID, exiting");
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const config = loadConfig();
|
|
32
|
-
const nc = await connectNats(config);
|
|
33
|
-
const sc = StringCodec();
|
|
34
|
-
try {
|
|
35
|
-
const js = nc.jetstream();
|
|
36
|
-
const kv = await js.views.kv("pending-hooks");
|
|
37
|
-
switch (event.hook_event_name) {
|
|
38
|
-
case "PermissionRequest":
|
|
39
|
-
await handlePermissionRequest(config, nc, kv, sc, event, taskId);
|
|
40
|
-
break;
|
|
41
|
-
case "Notification":
|
|
42
|
-
await handleNotification(config, nc, kv, sc, event, taskId);
|
|
43
|
-
break;
|
|
44
|
-
case "Stop":
|
|
45
|
-
await handleStop(config, nc, sc, taskId);
|
|
46
|
-
break;
|
|
47
|
-
default:
|
|
48
|
-
// Unknown hook, exit silently
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
finally {
|
|
53
|
-
await nc.drain();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
function permissionResponse(behavior) {
|
|
57
|
-
return {
|
|
58
|
-
hookSpecificOutput: {
|
|
59
|
-
hookEventName: "PermissionRequest",
|
|
60
|
-
decision: { behavior },
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
async function handlePermissionRequest(config, nc, kv, sc, event, taskId) {
|
|
65
|
-
const hookId = uuidv4();
|
|
66
|
-
const kvKey = `${config.agentId}.${taskId}.${hookId}`;
|
|
67
|
-
// Start watching BEFORE writing
|
|
68
|
-
const watch = await kv.watch({ key: kvKey });
|
|
69
|
-
// Write hook payload to KV
|
|
70
|
-
const payload = {
|
|
71
|
-
type: "permission",
|
|
72
|
-
task_id: taskId,
|
|
73
|
-
hook_id: hookId,
|
|
74
|
-
agent_id: config.agentId,
|
|
75
|
-
user_id: config.userId,
|
|
76
|
-
details: {
|
|
77
|
-
tool: event.tool_name,
|
|
78
|
-
input: event.tool_input,
|
|
79
|
-
},
|
|
80
|
-
status: "pending",
|
|
81
|
-
};
|
|
82
|
-
log(`permission: putting KV key=${kvKey} payload=${JSON.stringify(payload)}`);
|
|
83
|
-
await kv.put(kvKey, sc.encode(JSON.stringify(payload)));
|
|
84
|
-
// Publish push notification
|
|
85
|
-
nc.publish(`user.${config.userId}.push.request.permission`, sc.encode(JSON.stringify({
|
|
86
|
-
type: "permission",
|
|
87
|
-
task_id: taskId,
|
|
88
|
-
hook_id: hookId,
|
|
89
|
-
agent_id: config.agentId,
|
|
90
|
-
tool: event.tool_name,
|
|
91
|
-
input: event.tool_input,
|
|
92
|
-
})));
|
|
93
|
-
// Wait for status change
|
|
94
|
-
for await (const entry of watch) {
|
|
95
|
-
log(`permission: watch event op=${entry.operation} key=${entry.key}`);
|
|
96
|
-
if (entry.operation === "DEL" || entry.operation === "PURGE") {
|
|
97
|
-
// Key deleted, deny by default
|
|
98
|
-
log(`permission: key deleted/purged, denying`);
|
|
99
|
-
process.stdout.write(JSON.stringify(permissionResponse("deny")));
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
const updated = JSON.parse(sc.decode(entry.value));
|
|
104
|
-
log(`permission: KV update status=${updated.status} payload=${JSON.stringify(updated)}`);
|
|
105
|
-
if (updated.status === "confirmed" || updated.status === "allowed") {
|
|
106
|
-
const out = JSON.stringify(permissionResponse("allow"));
|
|
107
|
-
log(`permission: allowing, stdout=${out}`);
|
|
108
|
-
process.stdout.write(out);
|
|
109
|
-
await kv.delete(kvKey);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
else if (updated.status === "denied" || updated.status === "aborted") {
|
|
113
|
-
const out = JSON.stringify(permissionResponse("deny"));
|
|
114
|
-
log(`permission: denying, stdout=${out}`);
|
|
115
|
-
process.stdout.write(out);
|
|
116
|
-
await kv.delete(kvKey);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
// Still pending, keep watching
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
// Couldn't parse, keep watching
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
async function handleNotification(config, nc, kv, sc, event, taskId) {
|
|
127
|
-
const message = event.message || "";
|
|
128
|
-
// Check if notification requires user input
|
|
129
|
-
// Look for patterns suggesting input is needed
|
|
130
|
-
const inputPatterns = [
|
|
131
|
-
/\bwait(ing)?\s+(for|on)\s+(user\s+)?input\b/i,
|
|
132
|
-
/\bplease\s+(provide|enter|type|input)\b/i,
|
|
133
|
-
/\buser\s+input\s+(required|needed)\b/i,
|
|
134
|
-
/\bask(ing)?\s+(the\s+)?user\b/i,
|
|
135
|
-
/\brequires?\s+(user\s+)?input\b/i,
|
|
136
|
-
/\bprompt(ing)?\s+(the\s+)?user\b/i,
|
|
137
|
-
];
|
|
138
|
-
const needsInput = inputPatterns.some((pattern) => pattern.test(message));
|
|
139
|
-
if (!needsInput) {
|
|
140
|
-
// No input needed, exit silently
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
const hookId = uuidv4();
|
|
144
|
-
const kvKey = `${config.agentId}.${taskId}.${hookId}`;
|
|
145
|
-
// Start watching BEFORE writing
|
|
146
|
-
const watch = await kv.watch({ key: kvKey });
|
|
147
|
-
// Write hook payload to KV
|
|
148
|
-
const payload = {
|
|
149
|
-
type: "input",
|
|
150
|
-
task_id: taskId,
|
|
151
|
-
hook_id: hookId,
|
|
152
|
-
agent_id: config.agentId,
|
|
153
|
-
user_id: config.userId,
|
|
154
|
-
details: {
|
|
155
|
-
message: event.message,
|
|
156
|
-
},
|
|
157
|
-
status: "pending",
|
|
158
|
-
};
|
|
159
|
-
log(`input: putting KV key=${kvKey} payload=${JSON.stringify(payload)}`);
|
|
160
|
-
await kv.put(kvKey, sc.encode(JSON.stringify(payload)));
|
|
161
|
-
// Publish push notification
|
|
162
|
-
nc.publish(`user.${config.userId}.push.notify.input_needed`, sc.encode(JSON.stringify({
|
|
163
|
-
type: "input",
|
|
164
|
-
task_id: taskId,
|
|
165
|
-
hook_id: hookId,
|
|
166
|
-
agent_id: config.agentId,
|
|
167
|
-
message: event.message,
|
|
168
|
-
})));
|
|
169
|
-
// Wait for status change - the status field will contain the user's input text
|
|
170
|
-
for await (const entry of watch) {
|
|
171
|
-
log(`input: watch event op=${entry.operation} key=${entry.key}`);
|
|
172
|
-
if (entry.operation === "DEL" || entry.operation === "PURGE") {
|
|
173
|
-
log(`input: key deleted/purged`);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
try {
|
|
177
|
-
const updated = JSON.parse(sc.decode(entry.value));
|
|
178
|
-
log(`input: KV update status=${updated.status} payload=${JSON.stringify(updated)}`);
|
|
179
|
-
if (updated.status !== "pending") {
|
|
180
|
-
// The status field contains the user's input text
|
|
181
|
-
log(`input: resolved with user input`);
|
|
182
|
-
process.stdout.write(updated.status);
|
|
183
|
-
await kv.delete(kvKey);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
// Still pending, keep watching
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
// Couldn't parse, keep watching
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
async function handleStop(config, nc, sc, taskId) {
|
|
194
|
-
// Publish completion notification
|
|
195
|
-
nc.publish(`user.${config.userId}.push.notify.complete`, sc.encode(JSON.stringify({
|
|
196
|
-
type: "complete",
|
|
197
|
-
task_id: taskId,
|
|
198
|
-
agent_id: config.agentId,
|
|
199
|
-
})));
|
|
200
|
-
}
|
|
201
|
-
async function readStdin() {
|
|
202
|
-
const chunks = [];
|
|
203
|
-
for await (const chunk of process.stdin) {
|
|
204
|
-
chunks.push(chunk);
|
|
205
|
-
}
|
|
206
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
207
|
-
}
|
|
208
|
-
//# sourceMappingURL=hook.js.map
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Post-exit cleanup for a task process.
|
|
3
|
-
*
|
|
4
|
-
* Called by the platform hook (ExecStopPost on Linux, wrapper script on Windows)
|
|
5
|
-
* after the main `palmier run <taskId>` process exits.
|
|
6
|
-
*
|
|
7
|
-
* - If status.json shows "finish" or "fail", the process handled its own cleanup — no-op.
|
|
8
|
-
* - If status.json shows "abort", the RPC handler already wrote status and broadcast —
|
|
9
|
-
* just write the RESULT file and append history.
|
|
10
|
-
* - If status.json shows "start", the process died unexpectedly — write "fail" status,
|
|
11
|
-
* RESULT file, append history, and broadcast event.
|
|
12
|
-
*/
|
|
13
|
-
export declare function taskCleanupCommand(taskId: string): Promise<void>;
|
|
14
|
-
//# sourceMappingURL=task-cleanup.d.ts.map
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { loadConfig } from "../config.js";
|
|
4
|
-
import { connectNats } from "../nats-client.js";
|
|
5
|
-
import { getTaskDir, readTaskStatus, writeTaskStatus, appendHistory, parseTaskFile } from "../task.js";
|
|
6
|
-
import { publishHostEvent } from "../events.js";
|
|
7
|
-
/**
|
|
8
|
-
* Write a minimal RESULT file for a task that exited without writing one itself.
|
|
9
|
-
* Uses the status.json time_stamp as the start time.
|
|
10
|
-
*/
|
|
11
|
-
function writeCleanupResult(taskDir, taskName, runningState, startTime) {
|
|
12
|
-
const endTime = Date.now();
|
|
13
|
-
const resultFileName = `RESULT-${endTime}.md`;
|
|
14
|
-
// Find the task snapshot file that matches the start time
|
|
15
|
-
const taskSnapshotName = `TASK-${startTime}.md`;
|
|
16
|
-
const taskFile = fs.existsSync(path.join(taskDir, taskSnapshotName)) ? taskSnapshotName : "";
|
|
17
|
-
const content = `---\ntask_name: ${taskName}\nrunning_state: ${runningState}\nstart_time: ${startTime}\nend_time: ${endTime}\ntask_file: ${taskFile}\n---\nTask process exited unexpectedly.`;
|
|
18
|
-
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
19
|
-
return resultFileName;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Post-exit cleanup for a task process.
|
|
23
|
-
*
|
|
24
|
-
* Called by the platform hook (ExecStopPost on Linux, wrapper script on Windows)
|
|
25
|
-
* after the main `palmier run <taskId>` process exits.
|
|
26
|
-
*
|
|
27
|
-
* - If status.json shows "finish" or "fail", the process handled its own cleanup — no-op.
|
|
28
|
-
* - If status.json shows "abort", the RPC handler already wrote status and broadcast —
|
|
29
|
-
* just write the RESULT file and append history.
|
|
30
|
-
* - If status.json shows "start", the process died unexpectedly — write "fail" status,
|
|
31
|
-
* RESULT file, append history, and broadcast event.
|
|
32
|
-
*/
|
|
33
|
-
export async function taskCleanupCommand(taskId) {
|
|
34
|
-
const config = loadConfig();
|
|
35
|
-
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
36
|
-
const status = readTaskStatus(taskDir);
|
|
37
|
-
if (!status) {
|
|
38
|
-
console.log(`[task-cleanup] No status.json for task ${taskId}, nothing to do.`);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
// Process already handled its own cleanup
|
|
42
|
-
if (status.running_state === "finish" || status.running_state === "fail") {
|
|
43
|
-
console.log(`[task-cleanup] Task ${taskId} already in terminal state: ${status.running_state}`);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
// Read task name for RESULT file
|
|
47
|
-
let taskName = taskId;
|
|
48
|
-
try {
|
|
49
|
-
const task = parseTaskFile(taskDir);
|
|
50
|
-
taskName = task.frontmatter.name || taskId;
|
|
51
|
-
}
|
|
52
|
-
catch { /* use taskId as fallback name */ }
|
|
53
|
-
const startTime = status.time_stamp;
|
|
54
|
-
if (status.running_state === "abort") {
|
|
55
|
-
// RPC handler already wrote status and broadcast — just write RESULT + history
|
|
56
|
-
console.log(`[task-cleanup] Task ${taskId} was aborted via RPC, writing RESULT.`);
|
|
57
|
-
const resultFileName = writeCleanupResult(taskDir, taskName, "abort", startTime);
|
|
58
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
// status.running_state === "start" — unexpected death
|
|
62
|
-
console.log(`[task-cleanup] Task ${taskId} died unexpectedly, marking as failed.`);
|
|
63
|
-
writeTaskStatus(taskDir, { running_state: "fail", time_stamp: Date.now() });
|
|
64
|
-
const resultFileName = writeCleanupResult(taskDir, taskName, "fail", startTime);
|
|
65
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
66
|
-
// Broadcast failure event via NATS and/or HTTP, consistent with other status pushes
|
|
67
|
-
const mode = config.mode ?? "nats";
|
|
68
|
-
const useNats = mode === "nats" || mode === "auto";
|
|
69
|
-
const useHttp = mode === "lan" || mode === "auto";
|
|
70
|
-
let nc;
|
|
71
|
-
try {
|
|
72
|
-
if (useNats) {
|
|
73
|
-
nc = await connectNats(config);
|
|
74
|
-
}
|
|
75
|
-
const payload = { event_type: "running-state", running_state: "fail", name: taskName };
|
|
76
|
-
await publishHostEvent(nc, config, taskId, payload, useHttp);
|
|
77
|
-
}
|
|
78
|
-
finally {
|
|
79
|
-
if (nc && !nc.isClosed()) {
|
|
80
|
-
await nc.drain();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
//# sourceMappingURL=task-cleanup.js.map
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
You are a planning assistant 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/systemd.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { HostConfig } from "./types.js";
|
|
2
|
-
import type { ParsedTask } from "./types.js";
|
|
3
|
-
/**
|
|
4
|
-
* Convert a cron expression (5-field) to a systemd OnCalendar string.
|
|
5
|
-
* Handles basic cron patterns: minute hour day-of-month month day-of-week
|
|
6
|
-
*/
|
|
7
|
-
export declare function cronToOnCalendar(cron: string): string;
|
|
8
|
-
/**
|
|
9
|
-
* Install a systemd user timer + service for a task.
|
|
10
|
-
*/
|
|
11
|
-
export declare function installTaskTimer(config: HostConfig, task: ParsedTask): void;
|
|
12
|
-
/**
|
|
13
|
-
* Remove a task's systemd timer and service files.
|
|
14
|
-
*/
|
|
15
|
-
export declare function removeTaskTimer(taskId: string): void;
|
|
16
|
-
/**
|
|
17
|
-
* Run systemctl --user daemon-reload.
|
|
18
|
-
*/
|
|
19
|
-
export declare function daemonReload(): void;
|
|
20
|
-
//# sourceMappingURL=systemd.d.ts.map
|
package/dist/systemd.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
import { execSync } from "child_process";
|
|
5
|
-
const UNIT_DIR = path.join(homedir(), ".config", "systemd", "user");
|
|
6
|
-
function getTimerName(taskId) {
|
|
7
|
-
return `palmier-task-${taskId}.timer`;
|
|
8
|
-
}
|
|
9
|
-
function getServiceName(taskId) {
|
|
10
|
-
return `palmier-task-${taskId}.service`;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Convert a cron expression (5-field) to a systemd OnCalendar string.
|
|
14
|
-
* Handles basic cron patterns: minute hour day-of-month month day-of-week
|
|
15
|
-
*/
|
|
16
|
-
export function cronToOnCalendar(cron) {
|
|
17
|
-
const parts = cron.trim().split(/\s+/);
|
|
18
|
-
if (parts.length !== 5) {
|
|
19
|
-
throw new Error(`Invalid cron expression (expected 5 fields): ${cron}`);
|
|
20
|
-
}
|
|
21
|
-
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
22
|
-
// Map cron day-of-week names/numbers to systemd abbreviated names
|
|
23
|
-
const dowMap = {
|
|
24
|
-
"0": "Sun",
|
|
25
|
-
"1": "Mon",
|
|
26
|
-
"2": "Tue",
|
|
27
|
-
"3": "Wed",
|
|
28
|
-
"4": "Thu",
|
|
29
|
-
"5": "Fri",
|
|
30
|
-
"6": "Sat",
|
|
31
|
-
"7": "Sun",
|
|
32
|
-
};
|
|
33
|
-
// Convert day-of-week
|
|
34
|
-
let dow = dayOfWeek === "*" ? "*" : dayOfWeek;
|
|
35
|
-
if (dowMap[dow]) {
|
|
36
|
-
dow = dowMap[dow];
|
|
37
|
-
}
|
|
38
|
-
// Build OnCalendar string
|
|
39
|
-
// Format: DayOfWeek Year-Month-Day Hour:Minute:Second
|
|
40
|
-
const monthPart = month === "*" ? "*" : month.padStart(2, "0");
|
|
41
|
-
const dayPart = dayOfMonth === "*" ? "*" : dayOfMonth.padStart(2, "0");
|
|
42
|
-
const hourPart = hour === "*" ? "*" : hour.padStart(2, "0");
|
|
43
|
-
const minutePart = minute === "*" ? "*" : minute.padStart(2, "0");
|
|
44
|
-
if (dow === "*") {
|
|
45
|
-
return `*-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
|
|
46
|
-
}
|
|
47
|
-
return `${dow} *-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Install a systemd user timer + service for a task.
|
|
51
|
-
*/
|
|
52
|
-
export function installTaskTimer(config, task) {
|
|
53
|
-
fs.mkdirSync(UNIT_DIR, { recursive: true });
|
|
54
|
-
const taskId = task.frontmatter.id;
|
|
55
|
-
const serviceName = getServiceName(taskId);
|
|
56
|
-
const timerName = getTimerName(taskId);
|
|
57
|
-
// Determine the palmier binary path
|
|
58
|
-
const palmierBin = process.argv[1] || "palmier";
|
|
59
|
-
// Generate service unit
|
|
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
|
-
`;
|
|
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 || [];
|
|
74
|
-
const onCalendarLines = [];
|
|
75
|
-
for (const trigger of triggers) {
|
|
76
|
-
if (trigger.type === "cron") {
|
|
77
|
-
onCalendarLines.push(`OnCalendar=${cronToOnCalendar(trigger.value)}`);
|
|
78
|
-
}
|
|
79
|
-
else if (trigger.type === "once") {
|
|
80
|
-
onCalendarLines.push(`OnActiveSec=${trigger.value}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
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
|
|
93
|
-
`;
|
|
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
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Remove a task's systemd timer and service files.
|
|
107
|
-
*/
|
|
108
|
-
export function removeTaskTimer(taskId) {
|
|
109
|
-
const timerName = getTimerName(taskId);
|
|
110
|
-
const serviceName = getServiceName(taskId);
|
|
111
|
-
const timerPath = path.join(UNIT_DIR, timerName);
|
|
112
|
-
const servicePath = path.join(UNIT_DIR, serviceName);
|
|
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" });
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// Timer might not be running
|
|
120
|
-
}
|
|
121
|
-
try {
|
|
122
|
-
execSync(`systemctl --user disable ${timerName}`, { encoding: "utf-8" });
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
// Timer might not be enabled
|
|
126
|
-
}
|
|
127
|
-
fs.unlinkSync(timerPath);
|
|
128
|
-
}
|
|
129
|
-
if (fs.existsSync(servicePath))
|
|
130
|
-
fs.unlinkSync(servicePath);
|
|
131
|
-
daemonReload();
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Run systemctl --user daemon-reload.
|
|
135
|
-
*/
|
|
136
|
-
export function daemonReload() {
|
|
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
|
-
}
|
|
144
|
-
}
|
|
145
|
-
//# sourceMappingURL=systemd.js.map
|