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/hook.ts
DELETED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from "uuid";
|
|
2
|
-
import { StringCodec } from "nats";
|
|
3
|
-
import { loadConfig } from "../config.js";
|
|
4
|
-
import { connectNats } from "../nats-client.js";
|
|
5
|
-
import type { ClaudeHookEvent, HookPayload } from "../types.js";
|
|
6
|
-
import type { NatsConnection, KV } from "nats";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Handle a Claude Code hook invocation.
|
|
10
|
-
* Called by Claude Code as a subprocess. Reads hook event from stdin,
|
|
11
|
-
* dispatches by hook_name, and outputs response to stdout.
|
|
12
|
-
*/
|
|
13
|
-
export async function hookCommand(): Promise<void> {
|
|
14
|
-
const rawInput = await readStdin();
|
|
15
|
-
|
|
16
|
-
let event: ClaudeHookEvent;
|
|
17
|
-
try {
|
|
18
|
-
event = JSON.parse(rawInput) as ClaudeHookEvent;
|
|
19
|
-
} catch {
|
|
20
|
-
console.error("Failed to parse hook event from stdin");
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const taskId = process.env.PALMIER_TASK_ID;
|
|
25
|
-
if (!taskId) {
|
|
26
|
-
// Not running in a palmier task context, exit silently
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const config = loadConfig();
|
|
31
|
-
const nc = await connectNats(config);
|
|
32
|
-
const sc = StringCodec();
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const js = nc.jetstream();
|
|
36
|
-
const kv = await js.views.kv("pending-hooks");
|
|
37
|
-
|
|
38
|
-
switch (event.hook_name) {
|
|
39
|
-
case "PermissionRequest":
|
|
40
|
-
await handlePermissionRequest(config, nc, kv, sc, event, taskId);
|
|
41
|
-
break;
|
|
42
|
-
|
|
43
|
-
case "Notification":
|
|
44
|
-
await handleNotification(config, nc, kv, sc, event, taskId);
|
|
45
|
-
break;
|
|
46
|
-
|
|
47
|
-
case "Stop":
|
|
48
|
-
await handleStop(config, nc, sc, taskId);
|
|
49
|
-
break;
|
|
50
|
-
|
|
51
|
-
default:
|
|
52
|
-
// Unknown hook, exit silently
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
} finally {
|
|
56
|
-
await nc.drain();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function handlePermissionRequest(
|
|
61
|
-
config: ReturnType<typeof loadConfig>,
|
|
62
|
-
nc: NatsConnection,
|
|
63
|
-
kv: KV,
|
|
64
|
-
sc: ReturnType<typeof StringCodec>,
|
|
65
|
-
event: ClaudeHookEvent,
|
|
66
|
-
taskId: string
|
|
67
|
-
): Promise<void> {
|
|
68
|
-
const hookId = uuidv4();
|
|
69
|
-
const kvKey = `${config.agentId}.${taskId}.${hookId}`;
|
|
70
|
-
|
|
71
|
-
// Start watching BEFORE writing
|
|
72
|
-
const watch = await kv.watch({ key: kvKey });
|
|
73
|
-
|
|
74
|
-
// Write hook payload to KV
|
|
75
|
-
const payload: HookPayload = {
|
|
76
|
-
type: "permission",
|
|
77
|
-
task_id: taskId,
|
|
78
|
-
hook_id: hookId,
|
|
79
|
-
agent_id: config.agentId,
|
|
80
|
-
user_id: config.userId,
|
|
81
|
-
details: {
|
|
82
|
-
tool: event.tool_name,
|
|
83
|
-
input: event.tool_input,
|
|
84
|
-
},
|
|
85
|
-
status: "pending",
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
await kv.put(kvKey, sc.encode(JSON.stringify(payload)));
|
|
89
|
-
|
|
90
|
-
// Publish push notification
|
|
91
|
-
nc.publish(
|
|
92
|
-
`user.${config.userId}.push.request.permission`,
|
|
93
|
-
sc.encode(
|
|
94
|
-
JSON.stringify({
|
|
95
|
-
type: "permission",
|
|
96
|
-
task_id: taskId,
|
|
97
|
-
hook_id: hookId,
|
|
98
|
-
agent_id: config.agentId,
|
|
99
|
-
tool: event.tool_name,
|
|
100
|
-
input: event.tool_input,
|
|
101
|
-
})
|
|
102
|
-
)
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
// Wait for status change
|
|
106
|
-
for await (const entry of watch) {
|
|
107
|
-
if (entry.operation === "DEL" || entry.operation === "PURGE") {
|
|
108
|
-
// Key deleted, deny by default
|
|
109
|
-
process.stdout.write(JSON.stringify({ behavior: "deny" }));
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const updated = JSON.parse(sc.decode(entry.value)) as HookPayload;
|
|
115
|
-
if (updated.status === "confirmed" || updated.status === "allowed") {
|
|
116
|
-
process.stdout.write(JSON.stringify({ behavior: "allow" }));
|
|
117
|
-
await kv.delete(kvKey);
|
|
118
|
-
return;
|
|
119
|
-
} else if (updated.status === "denied" || updated.status === "aborted") {
|
|
120
|
-
process.stdout.write(JSON.stringify({ behavior: "deny" }));
|
|
121
|
-
await kv.delete(kvKey);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
// Still pending, keep watching
|
|
125
|
-
} catch {
|
|
126
|
-
// Couldn't parse, keep watching
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function handleNotification(
|
|
132
|
-
config: ReturnType<typeof loadConfig>,
|
|
133
|
-
nc: NatsConnection,
|
|
134
|
-
kv: KV,
|
|
135
|
-
sc: ReturnType<typeof StringCodec>,
|
|
136
|
-
event: ClaudeHookEvent,
|
|
137
|
-
taskId: string
|
|
138
|
-
): Promise<void> {
|
|
139
|
-
const message = event.message || "";
|
|
140
|
-
|
|
141
|
-
// Check if notification requires user input
|
|
142
|
-
// Look for patterns suggesting input is needed
|
|
143
|
-
const inputPatterns = [
|
|
144
|
-
/\bwait(ing)?\s+(for|on)\s+(user\s+)?input\b/i,
|
|
145
|
-
/\bplease\s+(provide|enter|type|input)\b/i,
|
|
146
|
-
/\buser\s+input\s+(required|needed)\b/i,
|
|
147
|
-
/\bask(ing)?\s+(the\s+)?user\b/i,
|
|
148
|
-
/\brequires?\s+(user\s+)?input\b/i,
|
|
149
|
-
/\bprompt(ing)?\s+(the\s+)?user\b/i,
|
|
150
|
-
];
|
|
151
|
-
|
|
152
|
-
const needsInput = inputPatterns.some((pattern) => pattern.test(message));
|
|
153
|
-
|
|
154
|
-
if (!needsInput) {
|
|
155
|
-
// No input needed, exit silently
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const hookId = uuidv4();
|
|
160
|
-
const kvKey = `${config.agentId}.${taskId}.${hookId}`;
|
|
161
|
-
|
|
162
|
-
// Start watching BEFORE writing
|
|
163
|
-
const watch = await kv.watch({ key: kvKey });
|
|
164
|
-
|
|
165
|
-
// Write hook payload to KV
|
|
166
|
-
const payload: HookPayload = {
|
|
167
|
-
type: "input",
|
|
168
|
-
task_id: taskId,
|
|
169
|
-
hook_id: hookId,
|
|
170
|
-
agent_id: config.agentId,
|
|
171
|
-
user_id: config.userId,
|
|
172
|
-
details: {
|
|
173
|
-
message: event.message,
|
|
174
|
-
},
|
|
175
|
-
status: "pending",
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
await kv.put(kvKey, sc.encode(JSON.stringify(payload)));
|
|
179
|
-
|
|
180
|
-
// Publish push notification
|
|
181
|
-
nc.publish(
|
|
182
|
-
`user.${config.userId}.push.notify.input_needed`,
|
|
183
|
-
sc.encode(
|
|
184
|
-
JSON.stringify({
|
|
185
|
-
type: "input",
|
|
186
|
-
task_id: taskId,
|
|
187
|
-
hook_id: hookId,
|
|
188
|
-
agent_id: config.agentId,
|
|
189
|
-
message: event.message,
|
|
190
|
-
})
|
|
191
|
-
)
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
// Wait for status change - the status field will contain the user's input text
|
|
195
|
-
for await (const entry of watch) {
|
|
196
|
-
if (entry.operation === "DEL" || entry.operation === "PURGE") {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
const updated = JSON.parse(sc.decode(entry.value)) as HookPayload;
|
|
202
|
-
if (updated.status !== "pending") {
|
|
203
|
-
// The status field contains the user's input text
|
|
204
|
-
process.stdout.write(updated.status);
|
|
205
|
-
await kv.delete(kvKey);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
// Still pending, keep watching
|
|
209
|
-
} catch {
|
|
210
|
-
// Couldn't parse, keep watching
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async function handleStop(
|
|
216
|
-
config: ReturnType<typeof loadConfig>,
|
|
217
|
-
nc: NatsConnection,
|
|
218
|
-
sc: ReturnType<typeof StringCodec>,
|
|
219
|
-
taskId: string
|
|
220
|
-
): Promise<void> {
|
|
221
|
-
// Publish completion notification
|
|
222
|
-
nc.publish(
|
|
223
|
-
`user.${config.userId}.push.notify.complete`,
|
|
224
|
-
sc.encode(
|
|
225
|
-
JSON.stringify({
|
|
226
|
-
type: "complete",
|
|
227
|
-
task_id: taskId,
|
|
228
|
-
agent_id: config.agentId,
|
|
229
|
-
})
|
|
230
|
-
)
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function readStdin(): Promise<string> {
|
|
235
|
-
const chunks: Buffer[] = [];
|
|
236
|
-
for await (const chunk of process.stdin) {
|
|
237
|
-
chunks.push(chunk as Buffer);
|
|
238
|
-
}
|
|
239
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
240
|
-
}
|