palmier 0.2.0 → 0.2.2
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 -1
- package/README.md +135 -45
- package/dist/agents/agent.d.ts +26 -0
- package/dist/agents/agent.js +32 -0
- package/dist/agents/claude.d.ts +8 -0
- package/dist/agents/claude.js +35 -0
- package/dist/agents/codex.d.ts +8 -0
- package/dist/agents/codex.js +41 -0
- package/dist/agents/gemini.d.ts +8 -0
- package/dist/agents/gemini.js +39 -0
- package/dist/agents/openclaw.d.ts +8 -0
- package/dist/agents/openclaw.js +25 -0
- package/dist/agents/shared-prompt.d.ts +11 -0
- package/dist/agents/shared-prompt.js +26 -0
- package/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.js +19 -0
- package/dist/commands/info.d.ts +5 -0
- package/dist/commands/info.js +40 -0
- package/dist/commands/init.d.ts +7 -2
- package/dist/commands/init.js +139 -49
- package/dist/commands/mcpserver.d.ts +2 -0
- package/dist/commands/mcpserver.js +75 -0
- package/dist/commands/pair.d.ts +6 -0
- package/dist/commands/pair.js +140 -0
- package/dist/commands/plan-generation.md +32 -0
- package/dist/commands/run.d.ts +0 -1
- package/dist/commands/run.js +258 -114
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +16 -228
- package/dist/commands/sessions.d.ts +4 -0
- package/dist/commands/sessions.js +30 -0
- package/dist/commands/task-generation.md +1 -1
- package/dist/config.d.ts +5 -5
- package/dist/config.js +24 -6
- package/dist/index.js +58 -5
- package/dist/nats-client.d.ts +3 -3
- package/dist/nats-client.js +2 -2
- package/dist/rpc-handler.d.ts +6 -0
- package/dist/rpc-handler.js +367 -0
- package/dist/session-store.d.ts +12 -0
- package/dist/session-store.js +57 -0
- package/dist/spawn-command.d.ts +26 -0
- package/dist/spawn-command.js +48 -0
- package/dist/systemd.d.ts +2 -2
- package/dist/task.d.ts +45 -2
- package/dist/task.js +155 -14
- package/dist/transports/http-transport.d.ts +6 -0
- package/dist/transports/http-transport.js +243 -0
- package/dist/transports/nats-transport.d.ts +6 -0
- package/dist/transports/nats-transport.js +69 -0
- package/dist/types.d.ts +30 -13
- package/package.json +4 -3
- package/src/agents/agent.ts +62 -0
- package/src/agents/claude.ts +39 -0
- package/src/agents/codex.ts +46 -0
- package/src/agents/gemini.ts +43 -0
- package/src/agents/openclaw.ts +29 -0
- package/src/agents/shared-prompt.ts +26 -0
- package/src/commands/agents.ts +20 -0
- package/src/commands/info.ts +44 -0
- package/src/commands/init.ts +229 -121
- package/src/commands/mcpserver.ts +92 -0
- package/src/commands/pair.ts +163 -0
- package/src/commands/plan-generation.md +32 -0
- package/src/commands/run.ts +323 -129
- package/src/commands/serve.ts +26 -287
- package/src/commands/sessions.ts +32 -0
- package/src/config.ts +30 -10
- package/src/index.ts +67 -6
- package/src/nats-client.ts +4 -4
- package/src/rpc-handler.ts +421 -0
- package/src/session-store.ts +68 -0
- package/src/spawn-command.ts +78 -0
- package/src/systemd.ts +2 -2
- package/src/task.ts +166 -16
- package/src/transports/http-transport.ts +290 -0
- package/src/transports/nats-transport.ts +82 -0
- package/src/types.ts +36 -13
- package/src/commands/task-generation.md +0 -28
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import { execSync, exec } from "child_process";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, getTaskCreatedAt, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList } from "./task.js";
|
|
11
|
+
import { installTaskTimer, removeTaskTimer } from "./systemd.js";
|
|
12
|
+
import { spawnCommand } from "./spawn-command.js";
|
|
13
|
+
import { getAgent } from "./agents/agent.js";
|
|
14
|
+
import { hasSessions, validateSession } from "./session-store.js";
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const PLAN_GENERATION_PROMPT = fs.readFileSync(path.join(__dirname, "commands", "plan-generation.md"), "utf-8");
|
|
17
|
+
function detectLanIp() {
|
|
18
|
+
const interfaces = os.networkInterfaces();
|
|
19
|
+
for (const name of Object.keys(interfaces)) {
|
|
20
|
+
for (const iface of interfaces[name] ?? []) {
|
|
21
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
|
22
|
+
return iface.address;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return "127.0.0.1";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse RESULT frontmatter into a metadata object.
|
|
30
|
+
*/
|
|
31
|
+
function parseResultFrontmatter(raw) {
|
|
32
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
33
|
+
if (!fmMatch)
|
|
34
|
+
return { content: raw };
|
|
35
|
+
const meta = {};
|
|
36
|
+
const requiredPermissions = [];
|
|
37
|
+
for (const line of fmMatch[1].split("\n")) {
|
|
38
|
+
const sep = line.indexOf(": ");
|
|
39
|
+
if (sep === -1)
|
|
40
|
+
continue;
|
|
41
|
+
const key = line.slice(0, sep).trim();
|
|
42
|
+
const value = line.slice(sep + 2).trim();
|
|
43
|
+
if (key === "required_permission") {
|
|
44
|
+
const pipeSep = value.indexOf("|");
|
|
45
|
+
if (pipeSep !== -1) {
|
|
46
|
+
requiredPermissions.push({ name: value.slice(0, pipeSep).trim(), description: value.slice(pipeSep + 1).trim() });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
requiredPermissions.push({ name: value, description: "" });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
meta[key] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const reportFiles = meta.report_files
|
|
57
|
+
? meta.report_files.split(",").map((f) => f.trim()).filter(Boolean)
|
|
58
|
+
: [];
|
|
59
|
+
return {
|
|
60
|
+
content: fmMatch[2],
|
|
61
|
+
task_name: meta.task_name,
|
|
62
|
+
running_state: meta.running_state,
|
|
63
|
+
start_time: meta.start_time ? Number(meta.start_time) : undefined,
|
|
64
|
+
end_time: meta.end_time ? Number(meta.end_time) : undefined,
|
|
65
|
+
task_file: meta.task_file,
|
|
66
|
+
report_files: reportFiles.length > 0 ? reportFiles : undefined,
|
|
67
|
+
required_permissions: requiredPermissions.length > 0 ? requiredPermissions : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Run plan generation for a task prompt using the given agent.
|
|
72
|
+
* Returns the generated plan body and task name.
|
|
73
|
+
*/
|
|
74
|
+
async function generatePlan(projectRoot, userPrompt, agentName) {
|
|
75
|
+
const fullPrompt = PLAN_GENERATION_PROMPT + userPrompt;
|
|
76
|
+
const planAgent = getAgent(agentName);
|
|
77
|
+
const { command, args } = planAgent.getPlanGenerationCommandLine(fullPrompt);
|
|
78
|
+
console.log(`[generatePlan] Running: ${command} ${args.join(" ")}`);
|
|
79
|
+
const { output } = await spawnCommand(command, args, {
|
|
80
|
+
cwd: projectRoot,
|
|
81
|
+
timeout: 120_000,
|
|
82
|
+
});
|
|
83
|
+
let name = "";
|
|
84
|
+
const trimmed = output.trim();
|
|
85
|
+
let body = trimmed;
|
|
86
|
+
const fmMatch = trimmed.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
87
|
+
if (fmMatch) {
|
|
88
|
+
try {
|
|
89
|
+
const fm = parseYaml(fmMatch[1]);
|
|
90
|
+
name = fm.task_name ?? "";
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// If frontmatter parsing fails, treat entire output as body
|
|
94
|
+
}
|
|
95
|
+
body = fmMatch[2].trimStart();
|
|
96
|
+
}
|
|
97
|
+
return { name, body };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Create a transport-agnostic RPC handler bound to the given config.
|
|
101
|
+
*/
|
|
102
|
+
export function createRpcHandler(config) {
|
|
103
|
+
function flattenTask(task) {
|
|
104
|
+
const taskDir = getTaskDir(config.projectRoot, task.frontmatter.id);
|
|
105
|
+
return {
|
|
106
|
+
...task.frontmatter,
|
|
107
|
+
body: task.body,
|
|
108
|
+
created_at: getTaskCreatedAt(taskDir),
|
|
109
|
+
status: readTaskStatus(taskDir),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function handleRpc(request) {
|
|
113
|
+
// Session token validation: if any sessions exist, require a valid token
|
|
114
|
+
if (hasSessions()) {
|
|
115
|
+
if (!request.sessionToken || !validateSession(request.sessionToken)) {
|
|
116
|
+
return { error: "Unauthorized" };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
switch (request.method) {
|
|
120
|
+
case "task.list": {
|
|
121
|
+
const tasks = listTasks(config.projectRoot);
|
|
122
|
+
return {
|
|
123
|
+
tasks: tasks.map((task) => flattenTask(task)),
|
|
124
|
+
agents: config.agents ?? [],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
case "task.create": {
|
|
128
|
+
const params = request.params;
|
|
129
|
+
// Short descriptions skip plan generation and use the description as-is
|
|
130
|
+
let name = "";
|
|
131
|
+
let body = "";
|
|
132
|
+
if (params.user_prompt.length < 50) {
|
|
133
|
+
name = params.user_prompt;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
try {
|
|
137
|
+
const plan = await generatePlan(config.projectRoot, params.user_prompt, params.agent);
|
|
138
|
+
name = plan.name;
|
|
139
|
+
body = plan.body;
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
const error = err;
|
|
143
|
+
return { error: "plan generation failed", stdout: error.stdout, stderr: error.stderr };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const id = randomUUID();
|
|
147
|
+
const taskDir = getTaskDir(config.projectRoot, id);
|
|
148
|
+
const task = {
|
|
149
|
+
frontmatter: {
|
|
150
|
+
id,
|
|
151
|
+
name,
|
|
152
|
+
user_prompt: params.user_prompt,
|
|
153
|
+
agent: params.agent,
|
|
154
|
+
triggers: params.triggers ?? [],
|
|
155
|
+
triggers_enabled: params.triggers_enabled ?? true,
|
|
156
|
+
requires_confirmation: params.requires_confirmation ?? true,
|
|
157
|
+
},
|
|
158
|
+
body,
|
|
159
|
+
};
|
|
160
|
+
writeTaskFile(taskDir, task);
|
|
161
|
+
appendTaskList(config.projectRoot, id);
|
|
162
|
+
if (task.frontmatter.triggers_enabled) {
|
|
163
|
+
installTaskTimer(config, task);
|
|
164
|
+
}
|
|
165
|
+
return flattenTask(task);
|
|
166
|
+
}
|
|
167
|
+
case "task.update": {
|
|
168
|
+
const params = request.params;
|
|
169
|
+
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
170
|
+
const existing = parseTaskFile(taskDir);
|
|
171
|
+
// Detect whether plan needs regeneration
|
|
172
|
+
const promptChanged = params.user_prompt !== undefined && params.user_prompt !== existing.frontmatter.user_prompt;
|
|
173
|
+
const agentChanged = params.agent !== undefined && params.agent !== existing.frontmatter.agent;
|
|
174
|
+
const needsRegeneration = promptChanged || agentChanged || !existing.body;
|
|
175
|
+
// Merge updates
|
|
176
|
+
if (params.user_prompt !== undefined)
|
|
177
|
+
existing.frontmatter.user_prompt = params.user_prompt;
|
|
178
|
+
if (params.agent !== undefined)
|
|
179
|
+
existing.frontmatter.agent = params.agent;
|
|
180
|
+
if (params.triggers !== undefined)
|
|
181
|
+
existing.frontmatter.triggers = params.triggers;
|
|
182
|
+
if (params.triggers_enabled !== undefined)
|
|
183
|
+
existing.frontmatter.triggers_enabled = params.triggers_enabled;
|
|
184
|
+
if (params.requires_confirmation !== undefined)
|
|
185
|
+
existing.frontmatter.requires_confirmation = params.requires_confirmation;
|
|
186
|
+
// Regenerate plan if needed
|
|
187
|
+
if (needsRegeneration) {
|
|
188
|
+
if (existing.frontmatter.user_prompt.length < 50) {
|
|
189
|
+
existing.frontmatter.name = existing.frontmatter.user_prompt;
|
|
190
|
+
existing.body = "";
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
try {
|
|
194
|
+
const plan = await generatePlan(config.projectRoot, existing.frontmatter.user_prompt, existing.frontmatter.agent);
|
|
195
|
+
existing.frontmatter.name = plan.name;
|
|
196
|
+
existing.body = plan.body;
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
const error = err;
|
|
200
|
+
return { error: "plan generation failed", stdout: error.stdout, stderr: error.stderr };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
writeTaskFile(taskDir, existing);
|
|
205
|
+
// Reinstall or remove timers based on triggers_enabled
|
|
206
|
+
removeTaskTimer(params.id);
|
|
207
|
+
if (existing.frontmatter.triggers_enabled) {
|
|
208
|
+
installTaskTimer(config, existing);
|
|
209
|
+
}
|
|
210
|
+
return flattenTask(existing);
|
|
211
|
+
}
|
|
212
|
+
case "task.delete": {
|
|
213
|
+
const params = request.params;
|
|
214
|
+
removeTaskTimer(params.id);
|
|
215
|
+
removeFromTaskList(config.projectRoot, params.id);
|
|
216
|
+
return { ok: true, task_id: params.id };
|
|
217
|
+
}
|
|
218
|
+
case "task.run": {
|
|
219
|
+
const params = request.params;
|
|
220
|
+
const serviceName = `palmier-task-${params.id}.service`;
|
|
221
|
+
try {
|
|
222
|
+
await execAsync(`systemctl --user start --no-block ${serviceName}`);
|
|
223
|
+
return { ok: true, task_id: params.id };
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
const e = err;
|
|
227
|
+
console.error(`task.run failed for ${params.id}: ${e.stderr || e.message}`);
|
|
228
|
+
return { error: `Failed to start task: ${e.stderr || e.message}` };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
case "task.abort": {
|
|
232
|
+
const params = request.params;
|
|
233
|
+
const serviceName = `palmier-task-${params.id}.service`;
|
|
234
|
+
try {
|
|
235
|
+
await execAsync(`systemctl --user stop ${serviceName}`);
|
|
236
|
+
return { ok: true, task_id: params.id };
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
const e = err;
|
|
240
|
+
console.error(`task.abort failed for ${params.id}: ${e.stderr || e.message}`);
|
|
241
|
+
return { error: `Failed to abort task: ${e.stderr || e.message}` };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
case "task.logs": {
|
|
245
|
+
const params = request.params;
|
|
246
|
+
const serviceName = `palmier-task-${params.id}.service`;
|
|
247
|
+
try {
|
|
248
|
+
const logs = execSync(`journalctl --user -u ${serviceName} -n 100 --no-pager`, { encoding: "utf-8" });
|
|
249
|
+
return { task_id: params.id, logs };
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
const error = err;
|
|
253
|
+
return { task_id: params.id, logs: error.stdout || "", error: error.stderr };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
case "task.status": {
|
|
257
|
+
const params = request.params;
|
|
258
|
+
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
259
|
+
const status = readTaskStatus(taskDir);
|
|
260
|
+
if (!status) {
|
|
261
|
+
return { task_id: params.id, error: "No status found" };
|
|
262
|
+
}
|
|
263
|
+
return { task_id: params.id, ...status };
|
|
264
|
+
}
|
|
265
|
+
case "task.result": {
|
|
266
|
+
const params = request.params;
|
|
267
|
+
if (!params.result_file) {
|
|
268
|
+
return { error: "result_file is required" };
|
|
269
|
+
}
|
|
270
|
+
const resultPath = path.join(config.projectRoot, "tasks", params.id, params.result_file);
|
|
271
|
+
try {
|
|
272
|
+
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
273
|
+
const meta = parseResultFrontmatter(raw);
|
|
274
|
+
return { task_id: params.id, ...meta };
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return { task_id: params.id, error: "No result file found" };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
case "task.reports": {
|
|
281
|
+
const params = request.params;
|
|
282
|
+
if (!Array.isArray(params.report_files) || params.report_files.length === 0) {
|
|
283
|
+
return { error: "report_files must be a non-empty array" };
|
|
284
|
+
}
|
|
285
|
+
const reports = [];
|
|
286
|
+
for (const file of params.report_files) {
|
|
287
|
+
if (!file.endsWith(".md")) {
|
|
288
|
+
reports.push({ file, error: "must end with .md" });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const basename = path.basename(file);
|
|
292
|
+
if (basename !== file) {
|
|
293
|
+
reports.push({ file, error: "must be a plain filename" });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const reportPath = path.join(config.projectRoot, "tasks", params.id, basename);
|
|
297
|
+
try {
|
|
298
|
+
const content = fs.readFileSync(reportPath, "utf-8");
|
|
299
|
+
reports.push({ file, content });
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
reports.push({ file, error: "Report file not found" });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return { task_id: params.id, reports };
|
|
306
|
+
}
|
|
307
|
+
case "task.user_input": {
|
|
308
|
+
const params = request.params;
|
|
309
|
+
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
310
|
+
const currentStatus = readTaskStatus(taskDir);
|
|
311
|
+
if (!currentStatus?.pending_confirmation && !currentStatus?.pending_permission?.length) {
|
|
312
|
+
return { ok: false, error: "not pending" };
|
|
313
|
+
}
|
|
314
|
+
writeTaskStatus(taskDir, { ...currentStatus, user_input: params.value });
|
|
315
|
+
console.log(`[task.user_input] ${params.id} → ${params.value}`);
|
|
316
|
+
return { ok: true };
|
|
317
|
+
}
|
|
318
|
+
case "activity.list": {
|
|
319
|
+
const params = request.params;
|
|
320
|
+
const { entries, total } = readHistory(config.projectRoot, {
|
|
321
|
+
offset: params.offset ?? 0,
|
|
322
|
+
limit: params.limit ?? 10,
|
|
323
|
+
task_id: params.task_id,
|
|
324
|
+
});
|
|
325
|
+
const enriched = entries.map((entry) => {
|
|
326
|
+
const resultPath = path.join(config.projectRoot, "tasks", entry.task_id, entry.result_file);
|
|
327
|
+
try {
|
|
328
|
+
const raw = fs.readFileSync(resultPath, "utf-8");
|
|
329
|
+
const meta = parseResultFrontmatter(raw);
|
|
330
|
+
// Exclude full content from list response
|
|
331
|
+
const { content: _, ...rest } = meta;
|
|
332
|
+
return { ...entry, ...rest };
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return { ...entry, error: "Result file not found" };
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
return { entries: enriched, total };
|
|
339
|
+
}
|
|
340
|
+
case "activity.delete": {
|
|
341
|
+
const params = request.params;
|
|
342
|
+
if (!params.task_id || !params.result_file) {
|
|
343
|
+
return { error: "task_id and result_file are required" };
|
|
344
|
+
}
|
|
345
|
+
const deleted = deleteHistoryEntry(config.projectRoot, params.task_id, params.result_file);
|
|
346
|
+
if (!deleted) {
|
|
347
|
+
return { error: "History entry not found" };
|
|
348
|
+
}
|
|
349
|
+
return { ok: true, task_id: params.task_id, result_file: params.result_file };
|
|
350
|
+
}
|
|
351
|
+
case "host.directInfo": {
|
|
352
|
+
if (config.mode === "lan" || config.mode === "auto") {
|
|
353
|
+
const ip = detectLanIp();
|
|
354
|
+
return {
|
|
355
|
+
directUrl: `http://${ip}:${config.directPort ?? 7400}`,
|
|
356
|
+
directToken: config.directToken,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return { directUrl: null, directToken: null };
|
|
360
|
+
}
|
|
361
|
+
default:
|
|
362
|
+
return { error: `Unknown method: ${request.method}` };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return handleRpc;
|
|
366
|
+
}
|
|
367
|
+
//# sourceMappingURL=rpc-handler.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SessionEntry {
|
|
2
|
+
token: string;
|
|
3
|
+
createdAt: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function loadSessions(): SessionEntry[];
|
|
7
|
+
export declare function addSession(label?: string): SessionEntry;
|
|
8
|
+
export declare function revokeSession(token: string): boolean;
|
|
9
|
+
export declare function revokeAllSessions(): number;
|
|
10
|
+
export declare function validateSession(token: string): boolean;
|
|
11
|
+
export declare function hasSessions(): boolean;
|
|
12
|
+
//# sourceMappingURL=session-store.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { randomBytes } from "crypto";
|
|
4
|
+
import { CONFIG_DIR } from "./config.js";
|
|
5
|
+
const SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
|
|
6
|
+
function readFile() {
|
|
7
|
+
try {
|
|
8
|
+
if (!fs.existsSync(SESSIONS_FILE))
|
|
9
|
+
return [];
|
|
10
|
+
const raw = fs.readFileSync(SESSIONS_FILE, "utf-8");
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function writeFile(sessions) {
|
|
18
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
19
|
+
fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2), "utf-8");
|
|
20
|
+
}
|
|
21
|
+
export function loadSessions() {
|
|
22
|
+
return readFile();
|
|
23
|
+
}
|
|
24
|
+
export function addSession(label) {
|
|
25
|
+
const sessions = readFile();
|
|
26
|
+
const entry = {
|
|
27
|
+
token: randomBytes(32).toString("hex"),
|
|
28
|
+
createdAt: new Date().toISOString(),
|
|
29
|
+
...(label ? { label } : {}),
|
|
30
|
+
};
|
|
31
|
+
sessions.push(entry);
|
|
32
|
+
writeFile(sessions);
|
|
33
|
+
return entry;
|
|
34
|
+
}
|
|
35
|
+
export function revokeSession(token) {
|
|
36
|
+
const sessions = readFile();
|
|
37
|
+
const idx = sessions.findIndex((s) => s.token === token);
|
|
38
|
+
if (idx === -1)
|
|
39
|
+
return false;
|
|
40
|
+
sessions.splice(idx, 1);
|
|
41
|
+
writeFile(sessions);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
export function revokeAllSessions() {
|
|
45
|
+
const sessions = readFile();
|
|
46
|
+
const count = sessions.length;
|
|
47
|
+
writeFile([]);
|
|
48
|
+
return count;
|
|
49
|
+
}
|
|
50
|
+
export function validateSession(token) {
|
|
51
|
+
const sessions = readFile();
|
|
52
|
+
return sessions.some((s) => s.token === token);
|
|
53
|
+
}
|
|
54
|
+
export function hasSessions() {
|
|
55
|
+
return readFile().length > 0;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=session-store.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SpawnCommandOptions {
|
|
2
|
+
cwd: string;
|
|
3
|
+
env?: Record<string, string>;
|
|
4
|
+
timeout?: number;
|
|
5
|
+
/** Echo stdout to process.stdout (useful for journald logging). */
|
|
6
|
+
echoStdout?: boolean;
|
|
7
|
+
/** Forward SIGINT/SIGTERM to the child and resolve on stop. */
|
|
8
|
+
forwardSignals?: boolean;
|
|
9
|
+
/** Resolve with output even on non-zero exit (instead of rejecting). */
|
|
10
|
+
resolveOnFailure?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Spawn a command with additional arguments.
|
|
14
|
+
*
|
|
15
|
+
* Runs without a shell — the command and args are passed directly to the
|
|
16
|
+
* child process (no escaping needed).
|
|
17
|
+
*
|
|
18
|
+
* stdin is set to "ignore" (equivalent to < /dev/null) because tools like
|
|
19
|
+
* `claude -p` hang indefinitely on an open stdin pipe.
|
|
20
|
+
*/
|
|
21
|
+
export interface SpawnCommandResult {
|
|
22
|
+
output: string;
|
|
23
|
+
exitCode: number | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function spawnCommand(command: string, args: string[], opts: SpawnCommandOptions): Promise<SpawnCommandResult>;
|
|
26
|
+
//# sourceMappingURL=spawn-command.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
export function spawnCommand(command, args, opts) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
const child = spawn(command, args, {
|
|
5
|
+
cwd: opts.cwd,
|
|
6
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7
|
+
env: opts.env ? { ...process.env, ...opts.env } : undefined,
|
|
8
|
+
});
|
|
9
|
+
const chunks = [];
|
|
10
|
+
child.stdout.on("data", (d) => {
|
|
11
|
+
chunks.push(d);
|
|
12
|
+
if (opts.echoStdout)
|
|
13
|
+
process.stdout.write(d);
|
|
14
|
+
});
|
|
15
|
+
child.stderr.on("data", (d) => process.stderr.write(d));
|
|
16
|
+
let stopping = false;
|
|
17
|
+
if (opts.forwardSignals) {
|
|
18
|
+
const killChild = () => {
|
|
19
|
+
stopping = true;
|
|
20
|
+
child.kill("SIGTERM");
|
|
21
|
+
};
|
|
22
|
+
process.on("SIGINT", killChild);
|
|
23
|
+
process.on("SIGTERM", killChild);
|
|
24
|
+
}
|
|
25
|
+
let timer;
|
|
26
|
+
if (opts.timeout) {
|
|
27
|
+
timer = setTimeout(() => {
|
|
28
|
+
child.kill();
|
|
29
|
+
reject(new Error("command timed out"));
|
|
30
|
+
}, opts.timeout);
|
|
31
|
+
}
|
|
32
|
+
child.on("close", (code) => {
|
|
33
|
+
if (timer)
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
const output = Buffer.concat(chunks).toString("utf-8");
|
|
36
|
+
if (code === 0 || stopping || opts.resolveOnFailure)
|
|
37
|
+
resolve({ output, exitCode: code });
|
|
38
|
+
else
|
|
39
|
+
reject(new Error(`process exited with code ${code}`));
|
|
40
|
+
});
|
|
41
|
+
child.on("error", (err) => {
|
|
42
|
+
if (timer)
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
reject(err);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=spawn-command.js.map
|
package/dist/systemd.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HostConfig } from "./types.js";
|
|
2
2
|
import type { ParsedTask } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Convert a cron expression (5-field) to a systemd OnCalendar string.
|
|
@@ -8,7 +8,7 @@ export declare function cronToOnCalendar(cron: string): string;
|
|
|
8
8
|
/**
|
|
9
9
|
* Install a systemd user timer + service for a task.
|
|
10
10
|
*/
|
|
11
|
-
export declare function installTaskTimer(config:
|
|
11
|
+
export declare function installTaskTimer(config: HostConfig, task: ParsedTask): void;
|
|
12
12
|
/**
|
|
13
13
|
* Remove a task's systemd timer and service files.
|
|
14
14
|
*/
|
package/dist/task.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ParsedTask } from "./types.js";
|
|
1
|
+
import type { ParsedTask, TaskStatus, HistoryEntry } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Parse a TASK.md file from the given task directory.
|
|
4
4
|
*/
|
|
@@ -9,11 +9,54 @@ export declare function parseTaskFile(taskDir: string): ParsedTask;
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function writeTaskFile(taskDir: string, task: ParsedTask): void;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* Append a task ID to the project-level tasks.jsonl file.
|
|
13
|
+
*/
|
|
14
|
+
export declare function appendTaskList(projectRoot: string, taskId: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Remove a task ID from the project-level tasks.jsonl file.
|
|
17
|
+
* Returns true if the entry was found and removed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function removeFromTaskList(projectRoot: string, taskId: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* List all tasks referenced in tasks.jsonl.
|
|
13
22
|
*/
|
|
14
23
|
export declare function listTasks(projectRoot: string): ParsedTask[];
|
|
15
24
|
/**
|
|
16
25
|
* Get the directory path for a task by its ID.
|
|
17
26
|
*/
|
|
18
27
|
export declare function getTaskDir(projectRoot: string, taskId: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Get the creation time (birthtime) of a TASK.md file in ms since epoch.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getTaskCreatedAt(taskDir: string): number;
|
|
32
|
+
/**
|
|
33
|
+
* Write task status to status.json in the task directory.
|
|
34
|
+
*/
|
|
35
|
+
export declare function writeTaskStatus(taskDir: string, status: TaskStatus): void;
|
|
36
|
+
/**
|
|
37
|
+
* Read task status from status.json in the task directory.
|
|
38
|
+
* Returns undefined if the file doesn't exist.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readTaskStatus(taskDir: string): TaskStatus | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Append a history entry to the project-level history.jsonl file.
|
|
43
|
+
*/
|
|
44
|
+
export declare function appendHistory(projectRoot: string, entry: HistoryEntry): void;
|
|
45
|
+
/**
|
|
46
|
+
* Delete a history entry and its associated result/task-snapshot files.
|
|
47
|
+
* Returns true if the entry was found and removed.
|
|
48
|
+
*/
|
|
49
|
+
export declare function deleteHistoryEntry(projectRoot: string, taskId: string, resultFile: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Read history entries from history.jsonl with pagination.
|
|
52
|
+
* Returns entries sorted most-recent-first.
|
|
53
|
+
*/
|
|
54
|
+
export declare function readHistory(projectRoot: string, opts: {
|
|
55
|
+
offset?: number;
|
|
56
|
+
limit?: number;
|
|
57
|
+
task_id?: string;
|
|
58
|
+
}): {
|
|
59
|
+
entries: HistoryEntry[];
|
|
60
|
+
total: number;
|
|
61
|
+
};
|
|
19
62
|
//# sourceMappingURL=task.d.ts.map
|