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/commands/run.ts
CHANGED
|
@@ -1,197 +1,205 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
process.on("
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { loadConfig } from "../config.js";
|
|
5
|
+
import { connectNats } from "../nats-client.js";
|
|
6
|
+
import { parseTaskFile, getTaskDir } from "../task.js";
|
|
7
|
+
import type { AgentConfig, ParsedTask, ConfirmPayload } from "../types.js";
|
|
8
|
+
import type { NatsConnection, KV } from "nats";
|
|
9
|
+
import { StringCodec } from "nats";
|
|
10
|
+
|
|
11
|
+
export type TaskEventType = "start" | "finish" | "abort" | "fail";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute a task by ID.
|
|
15
|
+
*/
|
|
16
|
+
export async function runCommand(taskId: string): Promise<void> {
|
|
17
|
+
const config = loadConfig();
|
|
18
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
19
|
+
const task = parseTaskFile(taskDir);
|
|
20
|
+
|
|
21
|
+
console.log(`Running task: ${taskId}`);
|
|
22
|
+
|
|
23
|
+
let nc: NatsConnection | undefined;
|
|
24
|
+
let confirmKv: KV | undefined;
|
|
25
|
+
const confirmKey = `${config.agentId}.${taskId}`;
|
|
26
|
+
|
|
27
|
+
const cleanup = async () => {
|
|
28
|
+
if (confirmKv) {
|
|
29
|
+
try { await confirmKv.delete(confirmKey); } catch { /* may not exist */ }
|
|
30
|
+
}
|
|
31
|
+
if (nc && !nc.isClosed()) {
|
|
32
|
+
await nc.drain();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Handle signals
|
|
37
|
+
const onSignal = async () => {
|
|
38
|
+
console.log("Received signal, cleaning up...");
|
|
39
|
+
if (eventKv) {
|
|
40
|
+
await writeTaskEvent(eventKv, `${config.agentId}.${taskId}`, "abort").catch(() => {});
|
|
41
|
+
}
|
|
42
|
+
await cleanup();
|
|
43
|
+
process.exit(1);
|
|
44
|
+
};
|
|
45
|
+
process.on("SIGINT", onSignal);
|
|
46
|
+
process.on("SIGTERM", onSignal);
|
|
47
|
+
|
|
48
|
+
let eventKv: KV | undefined;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
nc = await connectNats(config);
|
|
52
|
+
const js = nc.jetstream();
|
|
53
|
+
|
|
54
|
+
// Set up task-event KV and mark as started immediately
|
|
55
|
+
eventKv = await js.views.kv("task-event", { history: 1 });
|
|
56
|
+
const eventKey = `${config.agentId}.${taskId}`;
|
|
57
|
+
await writeTaskEvent(eventKv, eventKey, "start");
|
|
58
|
+
|
|
59
|
+
// If requires_confirmation, ask user via NATS KV
|
|
60
|
+
if (task.frontmatter.requires_confirmation) {
|
|
61
|
+
confirmKv = await js.views.kv("pending-confirmation");
|
|
62
|
+
|
|
63
|
+
const confirmed = await requestConfirmation(config, task, confirmKv);
|
|
64
|
+
if (!confirmed) {
|
|
65
|
+
console.log("Task aborted by user.");
|
|
66
|
+
await writeTaskEvent(eventKv, eventKey, "abort");
|
|
67
|
+
await cleanup();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.log("Task confirmed by user.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Spawn task process
|
|
74
|
+
const startTime = Date.now();
|
|
75
|
+
const output = await spawnTask(config, task);
|
|
76
|
+
const endTime = Date.now();
|
|
77
|
+
|
|
78
|
+
// Save result with frontmatter to task directory
|
|
79
|
+
const resultPath = path.join(taskDir, "RESULT.md");
|
|
80
|
+
const resultContent = `---\nstart_time: ${startTime}\nend_time: ${endTime}\n---\n${output}`;
|
|
81
|
+
fs.writeFileSync(resultPath, resultContent, "utf-8");
|
|
82
|
+
|
|
83
|
+
// Set event to finish on completion
|
|
84
|
+
await writeTaskEvent(eventKv, eventKey, "finish");
|
|
85
|
+
|
|
86
|
+
console.log(`Task ${taskId} completed.`);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(`Task ${taskId} failed:`, err);
|
|
89
|
+
if (eventKv) {
|
|
90
|
+
await writeTaskEvent(eventKv, `${config.agentId}.${taskId}`, "fail").catch(() => {});
|
|
91
|
+
}
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
} finally {
|
|
94
|
+
await cleanup();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sc = StringCodec();
|
|
99
|
+
|
|
100
|
+
async function writeTaskEvent(kv: KV, key: string, eventType: TaskEventType): Promise<void> {
|
|
101
|
+
const event = { event_type: eventType, time_stamp: Date.now() };
|
|
102
|
+
console.log(`[task-event] ${key} → ${eventType}`);
|
|
103
|
+
await kv.put(key, sc.encode(JSON.stringify(event)));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function requestConfirmation(
|
|
107
|
+
config: AgentConfig,
|
|
108
|
+
task: ParsedTask,
|
|
109
|
+
kv: KV,
|
|
110
|
+
): Promise<boolean> {
|
|
111
|
+
const kvKey = `${config.agentId}.${task.frontmatter.id}`;
|
|
112
|
+
|
|
113
|
+
// Write confirmation payload to KV — the server watches this bucket and sends push notifications
|
|
114
|
+
const payload: ConfirmPayload = {
|
|
115
|
+
type: "confirm",
|
|
116
|
+
task_id: task.frontmatter.id,
|
|
117
|
+
agent_id: config.agentId,
|
|
118
|
+
user_id: config.userId,
|
|
119
|
+
details: {
|
|
120
|
+
prompt: task.frontmatter.user_prompt,
|
|
121
|
+
},
|
|
122
|
+
status: "pending",
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await kv.put(kvKey, sc.encode(JSON.stringify(payload)));
|
|
126
|
+
|
|
127
|
+
// Watch AFTER writing — the initial history replay delivers the "pending" entry (skipped),
|
|
128
|
+
// then the iterator stays open for live updates (confirmed/aborted).
|
|
129
|
+
const watch = await kv.watch({ key: kvKey });
|
|
130
|
+
|
|
131
|
+
for await (const entry of watch) {
|
|
132
|
+
if (entry.operation === "DEL" || entry.operation === "PURGE") {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const updated = JSON.parse(sc.decode(entry.value)) as ConfirmPayload;
|
|
138
|
+
if (updated.status === "confirmed") return true;
|
|
139
|
+
if (updated.status === "aborted") return false;
|
|
140
|
+
// Still pending, keep watching
|
|
141
|
+
} catch {
|
|
142
|
+
// Couldn't parse, keep watching
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function shellEscape(arg: string): string {
|
|
150
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function spawnTask(
|
|
154
|
+
config: AgentConfig,
|
|
155
|
+
task: ParsedTask,
|
|
156
|
+
): Promise<string> {
|
|
157
|
+
return new Promise<string>((resolve, reject) => {
|
|
158
|
+
const prompt = task.body
|
|
159
|
+
? `${task.body}\n\n${task.frontmatter.user_prompt}`
|
|
160
|
+
: task.frontmatter.user_prompt;
|
|
161
|
+
|
|
162
|
+
const command = `${task.frontmatter.command_line} ${shellEscape(prompt)}`;
|
|
163
|
+
|
|
164
|
+
const child = spawn(command, {
|
|
165
|
+
cwd: config.projectRoot,
|
|
166
|
+
shell: true,
|
|
167
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
168
|
+
env: {
|
|
169
|
+
...process.env,
|
|
170
|
+
PALMIER_TASK_ID: task.frontmatter.id,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const stdoutChunks: Buffer[] = [];
|
|
175
|
+
|
|
176
|
+
child.stdout?.on("data", (data: Buffer) => {
|
|
177
|
+
stdoutChunks.push(data);
|
|
178
|
+
process.stdout.write(data);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
child.stderr?.on("data", (data: Buffer) => {
|
|
182
|
+
process.stderr.write(data);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
let stopping = false;
|
|
186
|
+
const killChild = () => {
|
|
187
|
+
stopping = true;
|
|
188
|
+
child.kill("SIGTERM");
|
|
189
|
+
};
|
|
190
|
+
process.on("SIGINT", killChild);
|
|
191
|
+
process.on("SIGTERM", killChild);
|
|
192
|
+
|
|
193
|
+
child.on("close", (exitCode) => {
|
|
194
|
+
if (exitCode === 0 || stopping) {
|
|
195
|
+
resolve(Buffer.concat(stdoutChunks).toString("utf-8"));
|
|
196
|
+
} else {
|
|
197
|
+
reject(new Error(`Task process exited with code ${exitCode}`));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
child.on("error", (err) => {
|
|
202
|
+
reject(err);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|