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/CLAUDE.md
ADDED
package/README.md
CHANGED
|
@@ -21,7 +21,6 @@ npm install -g palmier
|
|
|
21
21
|
| `palmier init --token <token>` | Provision the agent with a token from the dashboard |
|
|
22
22
|
| `palmier serve` | Run the persistent NATS RPC handler (default command) |
|
|
23
23
|
| `palmier run <task-id>` | Execute a specific task |
|
|
24
|
-
| `palmier hook` | Handle Claude Code hook events (invoked by Claude Code, not manually) |
|
|
25
24
|
|
|
26
25
|
## Setup
|
|
27
26
|
|
|
@@ -33,7 +32,6 @@ npm install -g palmier
|
|
|
33
32
|
The `init` command:
|
|
34
33
|
- Saves agent configuration to `~/.config/palmier/agent.json`
|
|
35
34
|
- Installs a systemd user service for the agent
|
|
36
|
-
- Configures Claude Code hooks for remote interaction
|
|
37
35
|
|
|
38
36
|
### Verifying the Service
|
|
39
37
|
|
|
@@ -66,9 +64,12 @@ cat ~/.config/palmier/agent.json
|
|
|
66
64
|
- The persistent process (`palmier serve`) is a NATS RPC handler. It derives the RPC method from the NATS subject (e.g., `...rpc.task.create` → `task.create`) and treats the message body as request parameters.
|
|
67
65
|
- **Task IDs** are generated by the agent as UUIDs.
|
|
68
66
|
- All RPC responses (`task.list`, `task.create`, `task.update`) return **flat task objects** — frontmatter fields at the top level, not nested under a `frontmatter` key.
|
|
67
|
+
- Tasks have no separate name field — the `user_prompt` is the primary identifier and display label.
|
|
68
|
+
- Plan generation is optional — if no plan body is present, the agent uses only `user_prompt` as the prompt.
|
|
69
|
+
- **Triggers can be enabled/disabled** via the `triggers_enabled` frontmatter field (default `true`). When disabled, systemd timers are removed; when re-enabled, they are reinstalled. Tasks can still be run manually regardless.
|
|
69
70
|
- Incoming tasks are stored as `TASK.md` files in a local `tasks/` directory.
|
|
70
|
-
- Task execution spawns **Claude Code
|
|
71
|
-
- **
|
|
71
|
+
- Task execution spawns **Claude Code as a background process** with `-p --dangerously-skip-permissions`, running non-interactively. Task lifecycle events (`start`, `finish`, `abort`, `fail`) are tracked via a NATS JetStream KV bucket (`task-event`), keyed by `<agent_id>.<task_id>`.
|
|
72
|
+
- **Task confirmation** — tasks with `requires_confirmation: true` write a pending entry to the `pending-confirmation` KV bucket before execution. The Web Server and PWA watch this bucket to show confirmation prompts. The task waits until the user confirms or aborts via the PWA or push notification.
|
|
72
73
|
|
|
73
74
|
## Project Structure
|
|
74
75
|
|
|
@@ -84,9 +85,54 @@ src/
|
|
|
84
85
|
init.ts # Provisioning logic
|
|
85
86
|
serve.ts # Persistent NATS RPC handler
|
|
86
87
|
run.ts # Single task execution
|
|
87
|
-
hook.ts # Claude Code hook handler
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
## Removing an Agent
|
|
91
|
+
|
|
92
|
+
To fully remove an agent from a machine:
|
|
93
|
+
|
|
94
|
+
1. **Delete the agent from the PWA dashboard** (this removes it from the server database).
|
|
95
|
+
|
|
96
|
+
2. **Stop and remove the systemd service:**
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
systemctl --user stop palmier-agent.service
|
|
100
|
+
systemctl --user disable palmier-agent.service
|
|
101
|
+
rm ~/.config/systemd/user/palmier-agent.service
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
3. **Remove any task timers and services:**
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
systemctl --user stop palmier-task-*.timer palmier-task-*.service 2>/dev/null
|
|
108
|
+
systemctl --user disable palmier-task-*.timer 2>/dev/null
|
|
109
|
+
rm -f ~/.config/systemd/user/palmier-task-*.timer ~/.config/systemd/user/palmier-task-*.service
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
4. **Reload systemd:**
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
systemctl --user daemon-reload
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
5. **Remove the agent configuration:**
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
rm -rf ~/.config/palmier
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
6. **Remove the tasks directory** from your project root:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
rm -rf tasks/
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
7. *(Optional)* **Disable login lingering** if no other user services need it:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
loginctl disable-linger
|
|
134
|
+
```
|
|
135
|
+
|
|
90
136
|
## Related
|
|
91
137
|
|
|
92
138
|
See the [palmier](../palmier) repo for the server, API, and PWA.
|
package/dist/commands/hook.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from "uuid";
|
|
2
2
|
import { StringCodec } from "nats";
|
|
3
|
+
import { appendFileSync } from "fs";
|
|
3
4
|
import { loadConfig } from "../config.js";
|
|
4
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
|
+
}
|
|
5
10
|
/**
|
|
6
11
|
* Handle a Claude Code hook invocation.
|
|
7
12
|
* Called by Claude Code as a subprocess. Reads hook event from stdin,
|
|
@@ -17,9 +22,10 @@ export async function hookCommand() {
|
|
|
17
22
|
console.error("Failed to parse hook event from stdin");
|
|
18
23
|
process.exit(1);
|
|
19
24
|
}
|
|
25
|
+
log(`received: ${JSON.stringify(event).slice(0, 500)}`);
|
|
20
26
|
const taskId = process.env.PALMIER_TASK_ID;
|
|
21
27
|
if (!taskId) {
|
|
22
|
-
|
|
28
|
+
log("no PALMIER_TASK_ID, exiting");
|
|
23
29
|
return;
|
|
24
30
|
}
|
|
25
31
|
const config = loadConfig();
|
|
@@ -28,7 +34,7 @@ export async function hookCommand() {
|
|
|
28
34
|
try {
|
|
29
35
|
const js = nc.jetstream();
|
|
30
36
|
const kv = await js.views.kv("pending-hooks");
|
|
31
|
-
switch (event.
|
|
37
|
+
switch (event.hook_event_name) {
|
|
32
38
|
case "PermissionRequest":
|
|
33
39
|
await handlePermissionRequest(config, nc, kv, sc, event, taskId);
|
|
34
40
|
break;
|
|
@@ -47,6 +53,14 @@ export async function hookCommand() {
|
|
|
47
53
|
await nc.drain();
|
|
48
54
|
}
|
|
49
55
|
}
|
|
56
|
+
function permissionResponse(behavior) {
|
|
57
|
+
return {
|
|
58
|
+
hookSpecificOutput: {
|
|
59
|
+
hookEventName: "PermissionRequest",
|
|
60
|
+
decision: { behavior },
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
50
64
|
async function handlePermissionRequest(config, nc, kv, sc, event, taskId) {
|
|
51
65
|
const hookId = uuidv4();
|
|
52
66
|
const kvKey = `${config.agentId}.${taskId}.${hookId}`;
|
|
@@ -65,6 +79,7 @@ async function handlePermissionRequest(config, nc, kv, sc, event, taskId) {
|
|
|
65
79
|
},
|
|
66
80
|
status: "pending",
|
|
67
81
|
};
|
|
82
|
+
log(`permission: putting KV key=${kvKey} payload=${JSON.stringify(payload)}`);
|
|
68
83
|
await kv.put(kvKey, sc.encode(JSON.stringify(payload)));
|
|
69
84
|
// Publish push notification
|
|
70
85
|
nc.publish(`user.${config.userId}.push.request.permission`, sc.encode(JSON.stringify({
|
|
@@ -77,20 +92,27 @@ async function handlePermissionRequest(config, nc, kv, sc, event, taskId) {
|
|
|
77
92
|
})));
|
|
78
93
|
// Wait for status change
|
|
79
94
|
for await (const entry of watch) {
|
|
95
|
+
log(`permission: watch event op=${entry.operation} key=${entry.key}`);
|
|
80
96
|
if (entry.operation === "DEL" || entry.operation === "PURGE") {
|
|
81
97
|
// Key deleted, deny by default
|
|
82
|
-
|
|
98
|
+
log(`permission: key deleted/purged, denying`);
|
|
99
|
+
process.stdout.write(JSON.stringify(permissionResponse("deny")));
|
|
83
100
|
return;
|
|
84
101
|
}
|
|
85
102
|
try {
|
|
86
103
|
const updated = JSON.parse(sc.decode(entry.value));
|
|
104
|
+
log(`permission: KV update status=${updated.status} payload=${JSON.stringify(updated)}`);
|
|
87
105
|
if (updated.status === "confirmed" || updated.status === "allowed") {
|
|
88
|
-
|
|
106
|
+
const out = JSON.stringify(permissionResponse("allow"));
|
|
107
|
+
log(`permission: allowing, stdout=${out}`);
|
|
108
|
+
process.stdout.write(out);
|
|
89
109
|
await kv.delete(kvKey);
|
|
90
110
|
return;
|
|
91
111
|
}
|
|
92
112
|
else if (updated.status === "denied" || updated.status === "aborted") {
|
|
93
|
-
|
|
113
|
+
const out = JSON.stringify(permissionResponse("deny"));
|
|
114
|
+
log(`permission: denying, stdout=${out}`);
|
|
115
|
+
process.stdout.write(out);
|
|
94
116
|
await kv.delete(kvKey);
|
|
95
117
|
return;
|
|
96
118
|
}
|
|
@@ -134,6 +156,7 @@ async function handleNotification(config, nc, kv, sc, event, taskId) {
|
|
|
134
156
|
},
|
|
135
157
|
status: "pending",
|
|
136
158
|
};
|
|
159
|
+
log(`input: putting KV key=${kvKey} payload=${JSON.stringify(payload)}`);
|
|
137
160
|
await kv.put(kvKey, sc.encode(JSON.stringify(payload)));
|
|
138
161
|
// Publish push notification
|
|
139
162
|
nc.publish(`user.${config.userId}.push.notify.input_needed`, sc.encode(JSON.stringify({
|
|
@@ -145,13 +168,17 @@ async function handleNotification(config, nc, kv, sc, event, taskId) {
|
|
|
145
168
|
})));
|
|
146
169
|
// Wait for status change - the status field will contain the user's input text
|
|
147
170
|
for await (const entry of watch) {
|
|
171
|
+
log(`input: watch event op=${entry.operation} key=${entry.key}`);
|
|
148
172
|
if (entry.operation === "DEL" || entry.operation === "PURGE") {
|
|
173
|
+
log(`input: key deleted/purged`);
|
|
149
174
|
return;
|
|
150
175
|
}
|
|
151
176
|
try {
|
|
152
177
|
const updated = JSON.parse(sc.decode(entry.value));
|
|
178
|
+
log(`input: KV update status=${updated.status} payload=${JSON.stringify(updated)}`);
|
|
153
179
|
if (updated.status !== "pending") {
|
|
154
180
|
// The status field contains the user's input text
|
|
181
|
+
log(`input: resolved with user input`);
|
|
155
182
|
process.stdout.write(updated.status);
|
|
156
183
|
await kv.delete(kvKey);
|
|
157
184
|
return;
|
package/dist/commands/init.js
CHANGED
|
@@ -53,38 +53,25 @@ export async function initCommand(options) {
|
|
|
53
53
|
saveConfig(config);
|
|
54
54
|
console.log(`Agent provisioned. ID: ${config.agentId}`);
|
|
55
55
|
console.log("Config saved to ~/.config/palmier/agent.json");
|
|
56
|
-
// 4.
|
|
57
|
-
const claudeSettingsDir = path.join(process.cwd(), ".claude");
|
|
58
|
-
fs.mkdirSync(claudeSettingsDir, { recursive: true });
|
|
59
|
-
const hookEntry = { hooks: [{ type: "command", command: "palmier hook" }] };
|
|
60
|
-
const hooksConfig = {
|
|
61
|
-
hooks: {
|
|
62
|
-
PermissionRequest: [hookEntry],
|
|
63
|
-
Notification: [hookEntry],
|
|
64
|
-
Stop: [hookEntry],
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
fs.writeFileSync(path.join(claudeSettingsDir, "settings.json"), JSON.stringify(hooksConfig, null, 2), "utf-8");
|
|
68
|
-
console.log("Claude Code hooks config written to .claude/settings.json");
|
|
69
|
-
// 5. Install systemd user service for palmier serve
|
|
56
|
+
// 4. Install systemd user service for palmier serve
|
|
70
57
|
const unitDir = path.join(homedir(), ".config", "systemd", "user");
|
|
71
58
|
fs.mkdirSync(unitDir, { recursive: true });
|
|
72
59
|
const palmierBin = process.argv[1] || "palmier";
|
|
73
|
-
const serviceContent = `[Unit]
|
|
74
|
-
Description=Palmier Agent
|
|
75
|
-
After=network-online.target
|
|
76
|
-
Wants=network-online.target
|
|
77
|
-
|
|
78
|
-
[Service]
|
|
79
|
-
Type=simple
|
|
80
|
-
ExecStart=${palmierBin} serve
|
|
81
|
-
WorkingDirectory=${config.projectRoot}
|
|
82
|
-
Restart=on-failure
|
|
83
|
-
RestartSec=5
|
|
84
|
-
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
85
|
-
|
|
86
|
-
[Install]
|
|
87
|
-
WantedBy=default.target
|
|
60
|
+
const serviceContent = `[Unit]
|
|
61
|
+
Description=Palmier Agent
|
|
62
|
+
After=network-online.target
|
|
63
|
+
Wants=network-online.target
|
|
64
|
+
|
|
65
|
+
[Service]
|
|
66
|
+
Type=simple
|
|
67
|
+
ExecStart=${palmierBin} serve
|
|
68
|
+
WorkingDirectory=${config.projectRoot}
|
|
69
|
+
Restart=on-failure
|
|
70
|
+
RestartSec=5
|
|
71
|
+
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
72
|
+
|
|
73
|
+
[Install]
|
|
74
|
+
WantedBy=default.target
|
|
88
75
|
`;
|
|
89
76
|
const servicePath = path.join(unitDir, "palmier-agent.service");
|
|
90
77
|
fs.writeFileSync(servicePath, serviceContent, "utf-8");
|
package/dist/commands/run.d.ts
CHANGED
package/dist/commands/run.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { spawn } from "child_process";
|
|
2
4
|
import { loadConfig } from "../config.js";
|
|
3
5
|
import { connectNats } from "../nats-client.js";
|
|
4
6
|
import { parseTaskFile, getTaskDir } from "../task.js";
|
|
@@ -10,21 +12,16 @@ export async function runCommand(taskId) {
|
|
|
10
12
|
const config = loadConfig();
|
|
11
13
|
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
12
14
|
const task = parseTaskFile(taskDir);
|
|
13
|
-
console.log(`Running task: ${
|
|
15
|
+
console.log(`Running task: ${taskId}`);
|
|
14
16
|
let nc;
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
const kvKeysToClean = [];
|
|
17
|
+
let confirmKv;
|
|
18
|
+
const confirmKey = `${config.agentId}.${taskId}`;
|
|
18
19
|
const cleanup = async () => {
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
await kv.delete(key);
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
// Ignore cleanup errors
|
|
26
|
-
}
|
|
20
|
+
if (confirmKv) {
|
|
21
|
+
try {
|
|
22
|
+
await confirmKv.delete(confirmKey);
|
|
27
23
|
}
|
|
24
|
+
catch { /* may not exist */ }
|
|
28
25
|
}
|
|
29
26
|
if (nc && !nc.isClosed()) {
|
|
30
27
|
await nc.drain();
|
|
@@ -33,84 +30,90 @@ export async function runCommand(taskId) {
|
|
|
33
30
|
// Handle signals
|
|
34
31
|
const onSignal = async () => {
|
|
35
32
|
console.log("Received signal, cleaning up...");
|
|
33
|
+
if (eventKv) {
|
|
34
|
+
await writeTaskEvent(eventKv, `${config.agentId}.${taskId}`, "abort").catch(() => { });
|
|
35
|
+
}
|
|
36
36
|
await cleanup();
|
|
37
37
|
process.exit(1);
|
|
38
38
|
};
|
|
39
39
|
process.on("SIGINT", onSignal);
|
|
40
40
|
process.on("SIGTERM", onSignal);
|
|
41
|
+
let eventKv;
|
|
41
42
|
try {
|
|
43
|
+
nc = await connectNats(config);
|
|
44
|
+
const js = nc.jetstream();
|
|
45
|
+
// Set up task-event KV and mark as started immediately
|
|
46
|
+
eventKv = await js.views.kv("task-event", { history: 1 });
|
|
47
|
+
const eventKey = `${config.agentId}.${taskId}`;
|
|
48
|
+
await writeTaskEvent(eventKv, eventKey, "start");
|
|
42
49
|
// If requires_confirmation, ask user via NATS KV
|
|
43
50
|
if (task.frontmatter.requires_confirmation) {
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
kv = await js.views.kv("pending-hooks");
|
|
47
|
-
const confirmed = await requestConfirmation(config, task, kv, nc, kvKeysToClean);
|
|
51
|
+
confirmKv = await js.views.kv("pending-confirmation");
|
|
52
|
+
const confirmed = await requestConfirmation(config, task, confirmKv);
|
|
48
53
|
if (!confirmed) {
|
|
49
54
|
console.log("Task aborted by user.");
|
|
55
|
+
await writeTaskEvent(eventKv, eventKey, "abort");
|
|
50
56
|
await cleanup();
|
|
51
57
|
return;
|
|
52
58
|
}
|
|
53
59
|
console.log("Task confirmed by user.");
|
|
54
60
|
}
|
|
55
|
-
// Spawn
|
|
56
|
-
|
|
61
|
+
// Spawn task process
|
|
62
|
+
const startTime = Date.now();
|
|
63
|
+
const output = await spawnTask(config, task);
|
|
64
|
+
const endTime = Date.now();
|
|
65
|
+
// Save result with frontmatter to task directory
|
|
66
|
+
const resultPath = path.join(taskDir, "RESULT.md");
|
|
67
|
+
const resultContent = `---\nstart_time: ${startTime}\nend_time: ${endTime}\n---\n${output}`;
|
|
68
|
+
fs.writeFileSync(resultPath, resultContent, "utf-8");
|
|
69
|
+
// Set event to finish on completion
|
|
70
|
+
await writeTaskEvent(eventKv, eventKey, "finish");
|
|
57
71
|
console.log(`Task ${taskId} completed.`);
|
|
58
72
|
}
|
|
59
73
|
catch (err) {
|
|
60
74
|
console.error(`Task ${taskId} failed:`, err);
|
|
75
|
+
if (eventKv) {
|
|
76
|
+
await writeTaskEvent(eventKv, `${config.agentId}.${taskId}`, "fail").catch(() => { });
|
|
77
|
+
}
|
|
61
78
|
process.exitCode = 1;
|
|
62
79
|
}
|
|
63
80
|
finally {
|
|
64
81
|
await cleanup();
|
|
65
82
|
}
|
|
66
83
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
const sc = StringCodec();
|
|
85
|
+
async function writeTaskEvent(kv, key, eventType) {
|
|
86
|
+
const event = { event_type: eventType, time_stamp: Date.now() };
|
|
87
|
+
console.log(`[task-event] ${key} → ${eventType}`);
|
|
88
|
+
await kv.put(key, sc.encode(JSON.stringify(event)));
|
|
89
|
+
}
|
|
90
|
+
async function requestConfirmation(config, task, kv) {
|
|
91
|
+
const kvKey = `${config.agentId}.${task.frontmatter.id}`;
|
|
92
|
+
// Write confirmation payload to KV — the server watches this bucket and sends push notifications
|
|
75
93
|
const payload = {
|
|
76
94
|
type: "confirm",
|
|
77
95
|
task_id: task.frontmatter.id,
|
|
78
|
-
hook_id: hookId,
|
|
79
96
|
agent_id: config.agentId,
|
|
80
97
|
user_id: config.userId,
|
|
81
98
|
details: {
|
|
82
|
-
task_name: task.frontmatter.name,
|
|
83
99
|
prompt: task.frontmatter.user_prompt,
|
|
84
100
|
},
|
|
85
101
|
status: "pending",
|
|
86
102
|
};
|
|
87
103
|
await kv.put(kvKey, sc.encode(JSON.stringify(payload)));
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
task_id: task.frontmatter.id,
|
|
92
|
-
hook_id: hookId,
|
|
93
|
-
agent_id: config.agentId,
|
|
94
|
-
task_name: task.frontmatter.name,
|
|
95
|
-
})));
|
|
96
|
-
// Wait for status change
|
|
104
|
+
// Watch AFTER writing — the initial history replay delivers the "pending" entry (skipped),
|
|
105
|
+
// then the iterator stays open for live updates (confirmed/aborted).
|
|
106
|
+
const watch = await kv.watch({ key: kvKey });
|
|
97
107
|
for await (const entry of watch) {
|
|
98
108
|
if (entry.operation === "DEL" || entry.operation === "PURGE") {
|
|
99
|
-
// Key was deleted, treat as aborted
|
|
100
109
|
return false;
|
|
101
110
|
}
|
|
102
111
|
try {
|
|
103
112
|
const updated = JSON.parse(sc.decode(entry.value));
|
|
104
|
-
if (updated.status === "confirmed")
|
|
105
|
-
await kv.delete(kvKey);
|
|
106
|
-
kvKeysToClean.pop();
|
|
113
|
+
if (updated.status === "confirmed")
|
|
107
114
|
return true;
|
|
108
|
-
|
|
109
|
-
else if (updated.status === "aborted") {
|
|
110
|
-
await kv.delete(kvKey);
|
|
111
|
-
kvKeysToClean.pop();
|
|
115
|
+
if (updated.status === "aborted")
|
|
112
116
|
return false;
|
|
113
|
-
}
|
|
114
117
|
// Still pending, keep watching
|
|
115
118
|
}
|
|
116
119
|
catch {
|
|
@@ -119,45 +122,50 @@ async function requestConfirmation(config, task, kv, nc, kvKeysToClean) {
|
|
|
119
122
|
}
|
|
120
123
|
return false;
|
|
121
124
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
function shellEscape(arg) {
|
|
126
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
127
|
+
}
|
|
128
|
+
async function spawnTask(config, task) {
|
|
125
129
|
return new Promise((resolve, reject) => {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (task.body) {
|
|
132
|
-
args.push("--system-prompt", task.body);
|
|
133
|
-
}
|
|
134
|
-
const ptyProcess = spawn("claude", args, {
|
|
135
|
-
name: "xterm-256color",
|
|
136
|
-
cols: 120,
|
|
137
|
-
rows: 40,
|
|
130
|
+
const prompt = task.body
|
|
131
|
+
? `${task.body}\n\n${task.frontmatter.user_prompt}`
|
|
132
|
+
: task.frontmatter.user_prompt;
|
|
133
|
+
const command = `${task.frontmatter.command_line} ${shellEscape(prompt)}`;
|
|
134
|
+
const child = spawn(command, {
|
|
138
135
|
cwd: config.projectRoot,
|
|
136
|
+
shell: true,
|
|
137
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
139
138
|
env: {
|
|
140
139
|
...process.env,
|
|
141
|
-
PALMIER_TASK_ID:
|
|
140
|
+
PALMIER_TASK_ID: task.frontmatter.id,
|
|
142
141
|
},
|
|
143
142
|
});
|
|
144
|
-
|
|
143
|
+
const stdoutChunks = [];
|
|
144
|
+
child.stdout?.on("data", (data) => {
|
|
145
|
+
stdoutChunks.push(data);
|
|
145
146
|
process.stdout.write(data);
|
|
146
147
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
resolve();
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
reject(new Error(`Claude exited with code ${exitCode}`));
|
|
153
|
-
}
|
|
148
|
+
child.stderr?.on("data", (data) => {
|
|
149
|
+
process.stderr.write(data);
|
|
154
150
|
});
|
|
155
|
-
|
|
151
|
+
let stopping = false;
|
|
156
152
|
const killChild = () => {
|
|
157
|
-
|
|
153
|
+
stopping = true;
|
|
154
|
+
child.kill("SIGTERM");
|
|
158
155
|
};
|
|
159
156
|
process.on("SIGINT", killChild);
|
|
160
157
|
process.on("SIGTERM", killChild);
|
|
158
|
+
child.on("close", (exitCode) => {
|
|
159
|
+
if (exitCode === 0 || stopping) {
|
|
160
|
+
resolve(Buffer.concat(stdoutChunks).toString("utf-8"));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
reject(new Error(`Task process exited with code ${exitCode}`));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
child.on("error", (err) => {
|
|
167
|
+
reject(err);
|
|
168
|
+
});
|
|
161
169
|
});
|
|
162
170
|
}
|
|
163
171
|
//# sourceMappingURL=run.js.map
|