palmier 0.8.0 → 0.8.3
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 +13 -0
- package/README.md +11 -11
- package/dist/agents/agent.d.ts +0 -4
- package/dist/agents/claude.js +1 -1
- package/dist/agents/codex.js +2 -2
- package/dist/agents/cursor.js +1 -1
- package/dist/agents/deepagents.js +1 -1
- package/dist/agents/gemini.js +3 -2
- package/dist/agents/goose.js +1 -1
- package/dist/agents/hermes.js +1 -1
- package/dist/agents/kiro.js +1 -1
- package/dist/agents/opencode.js +1 -1
- package/dist/agents/qoder.js +1 -1
- package/dist/agents/shared-prompt.d.ts +0 -3
- package/dist/agents/shared-prompt.js +0 -3
- package/dist/app-registry.d.ts +10 -0
- package/dist/app-registry.js +44 -0
- package/dist/commands/info.d.ts +0 -3
- package/dist/commands/info.js +0 -5
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.js +2 -11
- package/dist/commands/pair.d.ts +1 -4
- package/dist/commands/pair.js +1 -12
- package/dist/commands/restart.d.ts +0 -3
- package/dist/commands/restart.js +0 -3
- package/dist/commands/run.d.ts +1 -14
- package/dist/commands/run.js +18 -61
- package/dist/commands/serve.d.ts +0 -3
- package/dist/commands/serve.js +33 -27
- package/dist/config.d.ts +0 -8
- package/dist/config.js +0 -8
- package/dist/device-capabilities.d.ts +1 -1
- package/dist/event-queues.d.ts +6 -21
- package/dist/event-queues.js +6 -21
- package/dist/events.d.ts +0 -6
- package/dist/events.js +1 -9
- package/dist/index.js +0 -1
- package/dist/mcp-handler.js +1 -2
- package/dist/mcp-tools.d.ts +0 -3
- package/dist/mcp-tools.js +14 -18
- package/dist/nats-client.d.ts +0 -3
- package/dist/nats-client.js +1 -4
- package/dist/pending-requests.d.ts +4 -18
- package/dist/pending-requests.js +4 -18
- package/dist/platform/index.d.ts +1 -4
- package/dist/platform/index.js +1 -4
- package/dist/platform/linux.d.ts +3 -9
- package/dist/platform/linux.js +9 -20
- package/dist/platform/platform.d.ts +1 -4
- package/dist/platform/windows.d.ts +2 -5
- package/dist/platform/windows.js +19 -39
- package/dist/pwa/assets/index-B0F9mtid.css +1 -0
- package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
- package/dist/pwa/assets/{web-CF-N8Di6.js → web-C6lkQj9J.js} +1 -1
- package/dist/pwa/assets/{web-BpM3fNCn.js → web-Z1623me-.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.d.ts +0 -6
- package/dist/rpc-handler.js +19 -48
- package/dist/spawn-command.d.ts +10 -25
- package/dist/spawn-command.js +7 -15
- package/dist/task.d.ts +6 -64
- package/dist/task.js +7 -70
- package/dist/transports/http-transport.d.ts +0 -4
- package/dist/transports/http-transport.js +6 -28
- package/dist/transports/nats-transport.d.ts +0 -4
- package/dist/transports/nats-transport.js +3 -9
- package/dist/types.d.ts +3 -7
- package/dist/update-checker.d.ts +1 -4
- package/dist/update-checker.js +2 -5
- package/package.json +1 -1
- package/palmier-server/README.md +1 -1
- package/palmier-server/pwa/src/App.css +170 -20
- package/palmier-server/pwa/src/App.tsx +15 -1
- package/palmier-server/pwa/src/components/HostMenu.tsx +282 -473
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
- package/palmier-server/pwa/src/components/SessionsView.tsx +57 -25
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
- package/palmier-server/pwa/src/components/TaskCard.tsx +12 -4
- package/palmier-server/pwa/src/components/TaskForm.tsx +230 -33
- package/palmier-server/pwa/src/components/TasksView.tsx +5 -0
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +66 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
- package/palmier-server/pwa/src/pages/PairHost.tsx +18 -2
- package/palmier-server/pwa/src/types.ts +1 -1
- package/palmier-server/server/src/index.ts +7 -7
- package/palmier-server/server/src/routes/device.ts +4 -4
- package/palmier-server/spec.md +47 -6
- package/src/agents/agent.ts +0 -4
- package/src/agents/claude.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/cursor.ts +1 -1
- package/src/agents/deepagents.ts +1 -1
- package/src/agents/gemini.ts +3 -2
- package/src/agents/goose.ts +1 -1
- package/src/agents/hermes.ts +1 -1
- package/src/agents/kiro.ts +1 -1
- package/src/agents/opencode.ts +1 -1
- package/src/agents/qoder.ts +1 -1
- package/src/agents/shared-prompt.ts +0 -3
- package/src/app-registry.ts +52 -0
- package/src/commands/info.ts +0 -5
- package/src/commands/init.ts +2 -11
- package/src/commands/pair.ts +1 -12
- package/src/commands/restart.ts +0 -3
- package/src/commands/run.ts +18 -65
- package/src/commands/serve.ts +31 -27
- package/src/config.ts +0 -8
- package/src/device-capabilities.ts +4 -3
- package/src/event-queues.ts +6 -21
- package/src/events.ts +1 -9
- package/src/index.ts +0 -1
- package/src/mcp-handler.ts +1 -2
- package/src/mcp-tools.ts +14 -20
- package/src/nats-client.ts +1 -4
- package/src/pending-requests.ts +4 -18
- package/src/platform/index.ts +1 -4
- package/src/platform/linux.ts +9 -20
- package/src/platform/platform.ts +1 -4
- package/src/platform/windows.ts +19 -40
- package/src/rpc-handler.ts +20 -48
- package/src/spawn-command.ts +11 -27
- package/src/task.ts +7 -70
- package/src/transports/http-transport.ts +6 -39
- package/src/transports/nats-transport.ts +3 -9
- package/src/types.ts +3 -10
- package/src/update-checker.ts +2 -5
- package/test/task-parsing.test.ts +2 -3
- package/test/windows-xml.test.ts +11 -12
- package/dist/pwa/assets/index-FP1Mipr6.js +0 -120
- package/dist/pwa/assets/index-bLTn8zBj.css +0 -1
package/src/rpc-handler.ts
CHANGED
|
@@ -12,14 +12,12 @@ import { getAgent } from "./agents/agent.js";
|
|
|
12
12
|
import { validateClient } from "./client-store.js";
|
|
13
13
|
import { publishHostEvent } from "./events.js";
|
|
14
14
|
import { getCapabilityDevice, setCapabilityDevice, clearCapabilityDevice, type DeviceCapability } from "./device-capabilities.js";
|
|
15
|
+
import { listApps } from "./app-registry.js";
|
|
15
16
|
import { currentVersion, performUpdate } from "./update-checker.js";
|
|
16
17
|
import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
|
|
17
18
|
import { clearTaskQueue } from "./event-queues.js";
|
|
18
19
|
import type { HostConfig, ParsedTask, RpcMessage, ConversationMessage } from "./types.js";
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
-
* Parse RESULT frontmatter and conversation messages.
|
|
22
|
-
*/
|
|
23
21
|
export function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
24
22
|
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
25
23
|
if (!fmMatch) return { messages: [] };
|
|
@@ -33,19 +31,16 @@ export function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
|
33
31
|
|
|
34
32
|
const messages = parseConversationMessages(fmMatch[2]);
|
|
35
33
|
|
|
36
|
-
// Derive state from status messages — just look at the last one
|
|
37
34
|
const statusMessages = messages.filter((m: ConversationMessage) => m.role === "status");
|
|
38
35
|
const lastStatus = statusMessages[statusMessages.length - 1];
|
|
39
36
|
const startedMsg = statusMessages.find((m: ConversationMessage) => m.type === "started");
|
|
40
37
|
const terminalStates = ["finished", "failed", "aborted"];
|
|
41
38
|
const terminalMsg = [...statusMessages].reverse().find((m: ConversationMessage) => terminalStates.includes(m.type ?? ""));
|
|
42
39
|
|
|
43
|
-
// If last status is "started" (or continuation like "confirmation"/"monitoring"),
|
|
44
|
-
// determine if it's a task run or follow-up
|
|
45
40
|
const activeStates = ["started", "monitoring", "confirmation"];
|
|
46
41
|
let runningState: string | undefined;
|
|
47
42
|
if (lastStatus?.type === "monitoring") {
|
|
48
|
-
//
|
|
43
|
+
// Show "monitoring" only if no assistant/user message followed it.
|
|
49
44
|
const lastStatusIdx = messages.lastIndexOf(lastStatus);
|
|
50
45
|
const hasMessageAfter = messages.slice(lastStatusIdx + 1).some((m: ConversationMessage) => m.role === "assistant" || m.role === "user");
|
|
51
46
|
runningState = hasMessageAfter ? "started" : "monitoring";
|
|
@@ -65,16 +60,13 @@ export function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
|
65
60
|
};
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
/**
|
|
69
|
-
* Parse conversation messages from the body of a RESULT file.
|
|
70
|
-
*/
|
|
71
63
|
function parseConversationMessages(body: string): ConversationMessage[] {
|
|
72
64
|
const delimiterRegex = /<!-- palmier:message\s+(.*?)\s*-->/g;
|
|
73
65
|
const messages: ConversationMessage[] = [];
|
|
74
66
|
const matches = [...body.matchAll(delimiterRegex)];
|
|
75
67
|
|
|
76
68
|
if (matches.length === 0) {
|
|
77
|
-
// No delimiters — treat entire body as single assistant message
|
|
69
|
+
// No delimiters — treat entire body as a single assistant message.
|
|
78
70
|
const content = body.trim();
|
|
79
71
|
if (content) {
|
|
80
72
|
messages.push({ role: "assistant", time: 0, content });
|
|
@@ -106,10 +98,6 @@ function parseAttr(attrs: string, name: string): string | undefined {
|
|
|
106
98
|
return match ? match[1] : undefined;
|
|
107
99
|
}
|
|
108
100
|
|
|
109
|
-
/**
|
|
110
|
-
* Generate a concise task name from a user prompt using the given agent.
|
|
111
|
-
* Falls back to the raw prompt on failure.
|
|
112
|
-
*/
|
|
113
101
|
async function generateName(
|
|
114
102
|
projectRoot: string,
|
|
115
103
|
userPrompt: string,
|
|
@@ -136,9 +124,6 @@ async function generateName(
|
|
|
136
124
|
/** Active follow-up child processes, keyed by "taskId:runId". */
|
|
137
125
|
const activeFollowups = new Map<string, ChildProcess>();
|
|
138
126
|
|
|
139
|
-
/**
|
|
140
|
-
* Create a transport-agnostic RPC handler bound to the given config.
|
|
141
|
-
*/
|
|
142
127
|
export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
143
128
|
function flattenTask(task: ParsedTask) {
|
|
144
129
|
const taskDir = getTaskDir(config.projectRoot, task.frontmatter.id);
|
|
@@ -150,8 +135,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
150
135
|
}
|
|
151
136
|
|
|
152
137
|
async function handleRpc(request: RpcMessage): Promise<unknown> {
|
|
153
|
-
//
|
|
154
|
-
//
|
|
138
|
+
// task.user_input comes from server-originated push responses; it's gated
|
|
139
|
+
// by getPending() rather than a client token.
|
|
155
140
|
const skipAuth = request.method === "task.user_input";
|
|
156
141
|
if (!skipAuth && !request.localhost && (!request.clientToken || !validateClient(request.clientToken))) {
|
|
157
142
|
return { error: "Unauthorized" };
|
|
@@ -159,12 +144,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
159
144
|
|
|
160
145
|
switch (request.method) {
|
|
161
146
|
case "host.info": {
|
|
162
|
-
// Bootstrap metadata the PWA needs on connect, independent of which tab
|
|
163
|
-
// is active. Includes any prompts already waiting so a reconnecting
|
|
164
|
-
// PWA can render their modals without replaying events.
|
|
165
147
|
const capabilities: Record<string, string | null> = {};
|
|
166
|
-
for (const
|
|
167
|
-
capabilities[
|
|
148
|
+
for (const capability of ["location", "notifications", "sms-read", "sms-send", "contacts", "calendar", "alarm", "battery", "dnd", "send-email"] as const) {
|
|
149
|
+
capabilities[capability] = getCapabilityDevice(capability)?.clientToken ?? null;
|
|
168
150
|
}
|
|
169
151
|
return {
|
|
170
152
|
agents: config.agents ?? [],
|
|
@@ -250,11 +232,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
250
232
|
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
251
233
|
const existing = parseTaskFile(taskDir);
|
|
252
234
|
|
|
253
|
-
// Detect whether name needs regeneration
|
|
254
235
|
const promptChanged = params.user_prompt !== undefined && params.user_prompt !== existing.frontmatter.user_prompt;
|
|
255
236
|
const agentChanged = params.agent !== undefined && params.agent !== existing.frontmatter.agent;
|
|
256
237
|
|
|
257
|
-
// Merge updates
|
|
258
238
|
if (params.user_prompt !== undefined) existing.frontmatter.user_prompt = params.user_prompt;
|
|
259
239
|
if (params.agent !== undefined) existing.frontmatter.agent = params.agent;
|
|
260
240
|
if (params.schedule_type !== undefined) {
|
|
@@ -287,7 +267,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
287
267
|
}
|
|
288
268
|
}
|
|
289
269
|
|
|
290
|
-
// Regenerate name when prompt or agent changes
|
|
291
270
|
if (promptChanged || agentChanged) {
|
|
292
271
|
existing.frontmatter.name = existing.frontmatter.user_prompt.length <= 50
|
|
293
272
|
? existing.frontmatter.user_prompt
|
|
@@ -296,8 +275,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
296
275
|
|
|
297
276
|
writeTaskFile(taskDir, existing);
|
|
298
277
|
|
|
299
|
-
//
|
|
300
|
-
//
|
|
278
|
+
// installTaskTimer overwrites in-place (schtasks /f, systemd unit rewrite)
|
|
279
|
+
// without killing a running task process.
|
|
301
280
|
getPlatform().installTaskTimer(config, existing);
|
|
302
281
|
|
|
303
282
|
return flattenTask(existing);
|
|
@@ -341,13 +320,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
341
320
|
};
|
|
342
321
|
|
|
343
322
|
writeTaskFile(taskDir, task);
|
|
344
|
-
//
|
|
323
|
+
// One-off run: do NOT append to tasks.jsonl.
|
|
345
324
|
|
|
346
|
-
// Create initial result file so it appears in runs list immediately
|
|
347
325
|
const runId = createRunDir(taskDir, name, Date.now(), params.agent);
|
|
348
326
|
appendHistory(config.projectRoot, { task_id: id, run_id: runId });
|
|
349
327
|
|
|
350
|
-
// Spawn `palmier run <id>` directly as a detached process
|
|
351
328
|
const script = process.argv[1] || "palmier";
|
|
352
329
|
const child = spawn(process.execPath, [script, "run", id], {
|
|
353
330
|
detached: true,
|
|
@@ -365,13 +342,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
365
342
|
const runTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
366
343
|
const platform = getPlatform();
|
|
367
344
|
|
|
368
|
-
// If the task is already running, kill the stale process and start fresh
|
|
369
345
|
if (platform.isTaskRunning(params.id)) {
|
|
370
346
|
console.log(`[task.run] Task ${params.id} is already running, killing stale process`);
|
|
371
347
|
await platform.stopTask(params.id);
|
|
372
348
|
}
|
|
373
349
|
|
|
374
|
-
// Create initial result file so it appears in runs list immediately
|
|
375
350
|
const runTask = parseTaskFile(runTaskDir);
|
|
376
351
|
const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now(), runTask.frontmatter.agent);
|
|
377
352
|
appendHistory(config.projectRoot, { task_id: params.id, run_id: taskRunId });
|
|
@@ -399,7 +374,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
399
374
|
const followupTask = parseTaskFile(followupTaskDir);
|
|
400
375
|
const followupRunDir = getRunDir(followupTaskDir, params.run_id);
|
|
401
376
|
|
|
402
|
-
// Append user message + started status
|
|
403
377
|
appendRunMessage(followupTaskDir, params.run_id, {
|
|
404
378
|
role: "user",
|
|
405
379
|
time: Date.now(),
|
|
@@ -413,13 +387,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
413
387
|
});
|
|
414
388
|
await publishHostEvent(nc, config.hostId, params.id, { event_type: "result-updated", run_id: params.run_id });
|
|
415
389
|
|
|
416
|
-
// Fire-and-forget: invoke agent inline as a child of the serve process
|
|
417
390
|
const followupAgent = getAgent(followupTask.frontmatter.agent);
|
|
418
391
|
const { command: cmd, args: cmdArgs, stdin, env: followupAgentEnv } = followupAgent.getTaskRunCommandLine(
|
|
419
392
|
followupTask, params.message, followupTask.frontmatter.yolo_mode ? "yolo" : followupTask.frontmatter.permissions,
|
|
420
393
|
);
|
|
421
394
|
|
|
422
|
-
// Spawn directly via crossSpawn so we can track and kill the child
|
|
423
395
|
const child = crossSpawn(cmd, cmdArgs, {
|
|
424
396
|
cwd: followupRunDir,
|
|
425
397
|
stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
@@ -429,14 +401,13 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
429
401
|
if (stdin != null) child.stdin!.end(stdin);
|
|
430
402
|
activeFollowups.set(followupKey, child);
|
|
431
403
|
|
|
432
|
-
// Collect output
|
|
433
404
|
const chunks: Buffer[] = [];
|
|
434
405
|
child.stdout?.on("data", (d: Buffer) => chunks.push(d));
|
|
435
406
|
child.stderr?.on("data", (d: Buffer) => process.stderr.write(d));
|
|
436
407
|
|
|
437
408
|
child.on("close", async (code: number | null) => {
|
|
438
409
|
activeFollowups.delete(followupKey);
|
|
439
|
-
//
|
|
410
|
+
// stop_followup already wrote the stopped status.
|
|
440
411
|
if (child.killed) return;
|
|
441
412
|
|
|
442
413
|
const output = Buffer.concat(chunks).toString("utf-8");
|
|
@@ -484,7 +455,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
484
455
|
return { error: "No active follow-up for this run" };
|
|
485
456
|
}
|
|
486
457
|
|
|
487
|
-
// Kill the child process tree
|
|
488
458
|
if (process.platform === "win32" && child.pid) {
|
|
489
459
|
try {
|
|
490
460
|
const { execFileSync } = await import("child_process");
|
|
@@ -494,7 +464,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
494
464
|
child.kill();
|
|
495
465
|
}
|
|
496
466
|
|
|
497
|
-
//
|
|
467
|
+
// child.killed stops the close handler from double-writing the status.
|
|
498
468
|
const stopTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
499
469
|
appendRunMessage(stopTaskDir, params.run_id, {
|
|
500
470
|
role: "status",
|
|
@@ -510,17 +480,16 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
510
480
|
case "task.abort": {
|
|
511
481
|
const params = request.params as { id: string };
|
|
512
482
|
const abortTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
513
|
-
// Read
|
|
514
|
-
//
|
|
483
|
+
// Read PID before overwriting — stopTask needs it to kill the
|
|
484
|
+
// process tree on Windows.
|
|
515
485
|
const abortPrevStatus = readTaskStatus(abortTaskDir);
|
|
516
|
-
// Write abort status
|
|
517
|
-
// handler
|
|
486
|
+
// Write abort status before killing so the dying process's signal
|
|
487
|
+
// handler sees this was RPC-initiated and skips publishing.
|
|
518
488
|
writeTaskStatus(abortTaskDir, {
|
|
519
489
|
running_state: "aborted",
|
|
520
490
|
time_stamp: Date.now(),
|
|
521
491
|
...(abortPrevStatus?.pid ? { pid: abortPrevStatus.pid } : {}),
|
|
522
492
|
});
|
|
523
|
-
// Append aborted status to the latest run
|
|
524
493
|
try {
|
|
525
494
|
const runDirs = fs.readdirSync(abortTaskDir)
|
|
526
495
|
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(abortTaskDir, f, "TASKRUN.md")))
|
|
@@ -543,7 +512,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
543
512
|
console.error(`task.abort failed for ${params.id}: ${e.stderr || e.message}`);
|
|
544
513
|
return { error: `Failed to abort task: ${e.stderr || e.message}` };
|
|
545
514
|
}
|
|
546
|
-
// Notify connected clients (NATS + HTTP SSE if LAN server is running)
|
|
547
515
|
const abortPayload: Record<string, unknown> = { event_type: "running-state", running_state: "aborted" };
|
|
548
516
|
await publishHostEvent(nc, config.hostId, params.id, abortPayload);
|
|
549
517
|
return { ok: true, task_id: params.id };
|
|
@@ -694,6 +662,10 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
694
662
|
return { ok: true };
|
|
695
663
|
}
|
|
696
664
|
|
|
665
|
+
case "device.notifications.apps": {
|
|
666
|
+
return { apps: listApps() };
|
|
667
|
+
}
|
|
668
|
+
|
|
697
669
|
default:
|
|
698
670
|
return { error: `Unknown method: ${request.method}` };
|
|
699
671
|
}
|
package/src/spawn-command.ts
CHANGED
|
@@ -18,19 +18,12 @@ export interface SpawnStreamingOptions {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Spawn
|
|
22
|
-
* with stdout piped
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* shell: true is required so users can write piped commands like
|
|
28
|
-
* "tail -f log | grep ERROR".
|
|
29
|
-
*
|
|
30
|
-
* stdin is "pipe" (kept open, never written to) rather than "ignore"
|
|
31
|
-
* (/dev/null). Some long-running commands exit when stdin is closed/EOF.
|
|
32
|
-
* This differs from spawnCommand() which uses "ignore" because agent
|
|
33
|
-
* CLIs like `claude -p` hang on an open stdin pipe.
|
|
21
|
+
* Spawn with shell interpretation for piped commands like "tail -f log | grep".
|
|
22
|
+
* Returns the ChildProcess with stdout piped so the caller can read it directly
|
|
23
|
+
* (contrast with spawnCommand which buffers). stdin is "pipe" (held open, not
|
|
24
|
+
* written to): some long-running commands exit on stdin EOF. Agent CLIs like
|
|
25
|
+
* `claude -p` conversely hang on an open stdin, which is why spawnCommand
|
|
26
|
+
* defaults to "ignore".
|
|
34
27
|
*/
|
|
35
28
|
export function spawnStreamingCommand(
|
|
36
29
|
command: string,
|
|
@@ -60,18 +53,10 @@ export interface SpawnCommandOptions {
|
|
|
60
53
|
}
|
|
61
54
|
|
|
62
55
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* On other platforms the command is executed directly (no shell), so no
|
|
69
|
-
* escaping is needed.
|
|
70
|
-
*
|
|
71
|
-
* stdin is set to "ignore" by default (equivalent to < /dev/null) because
|
|
72
|
-
* tools like `claude -p` hang indefinitely on an open stdin pipe.
|
|
73
|
-
* When opts.stdin is provided, stdin is set to "pipe" and the string is
|
|
74
|
-
* written to the process before closing the pipe.
|
|
56
|
+
* cross-spawn resolves .cmd shims and escapes args on Windows without shell:true
|
|
57
|
+
* (which mishandles special characters). stdin defaults to "ignore" because
|
|
58
|
+
* tools like `claude -p` hang on an open stdin pipe; pass opts.stdin to write
|
|
59
|
+
* a string and then close the pipe.
|
|
75
60
|
*/
|
|
76
61
|
export interface SpawnCommandResult {
|
|
77
62
|
output: string;
|
|
@@ -84,8 +69,7 @@ export function spawnCommand(
|
|
|
84
69
|
opts: SpawnCommandOptions,
|
|
85
70
|
): Promise<SpawnCommandResult> {
|
|
86
71
|
return new Promise<SpawnCommandResult>((resolve, reject) => {
|
|
87
|
-
//
|
|
88
|
-
// in arguments, and CLI prompts don't need them.
|
|
72
|
+
// cmd.exe can't handle literal newlines in arguments.
|
|
89
73
|
const finalArgs = process.platform === "win32"
|
|
90
74
|
? args.map((a) => a.replace(/[\r\n]+/g, " "))
|
|
91
75
|
: args;
|
package/src/task.ts
CHANGED
|
@@ -3,9 +3,6 @@ import * as path from "path";
|
|
|
3
3
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
4
4
|
import type { ParsedTask, TaskFrontmatter, TaskStatus, HistoryEntry, ConversationMessage } from "./types.js";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Parse a TASK.md file from the given task directory.
|
|
8
|
-
*/
|
|
9
6
|
export function parseTaskFile(taskDir: string): ParsedTask {
|
|
10
7
|
const filePath = path.join(taskDir, "TASK.md");
|
|
11
8
|
|
|
@@ -17,9 +14,6 @@ export function parseTaskFile(taskDir: string): ParsedTask {
|
|
|
17
14
|
return parseTaskContent(content);
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
/**
|
|
21
|
-
* Parse TASK.md content string into frontmatter + body.
|
|
22
|
-
*/
|
|
23
17
|
export function parseTaskContent(content: string): ParsedTask {
|
|
24
18
|
const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
25
19
|
const match = content.match(fmRegex);
|
|
@@ -41,10 +35,6 @@ export function parseTaskContent(content: string): ParsedTask {
|
|
|
41
35
|
return { frontmatter };
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
/**
|
|
45
|
-
* Write a TASK.md file to the given task directory.
|
|
46
|
-
* Creates the directory if it doesn't exist.
|
|
47
|
-
*/
|
|
48
38
|
export function writeTaskFile(taskDir: string, task: ParsedTask): void {
|
|
49
39
|
fs.mkdirSync(taskDir, { recursive: true });
|
|
50
40
|
|
|
@@ -55,18 +45,11 @@ export function writeTaskFile(taskDir: string, task: ParsedTask): void {
|
|
|
55
45
|
fs.writeFileSync(filePath, content, "utf-8");
|
|
56
46
|
}
|
|
57
47
|
|
|
58
|
-
/**
|
|
59
|
-
* Append a task ID to the project-level tasks.jsonl file.
|
|
60
|
-
*/
|
|
61
48
|
export function appendTaskList(projectRoot: string, taskId: string): void {
|
|
62
49
|
const listPath = path.join(projectRoot, "tasks.jsonl");
|
|
63
50
|
fs.appendFileSync(listPath, JSON.stringify({ task_id: taskId }) + "\n", "utf-8");
|
|
64
51
|
}
|
|
65
52
|
|
|
66
|
-
/**
|
|
67
|
-
* Remove a task ID from the project-level tasks.jsonl file.
|
|
68
|
-
* Returns true if the entry was found and removed.
|
|
69
|
-
*/
|
|
70
53
|
export function removeFromTaskList(projectRoot: string, taskId: string): boolean {
|
|
71
54
|
const listPath = path.join(projectRoot, "tasks.jsonl");
|
|
72
55
|
if (!fs.existsSync(listPath)) return false;
|
|
@@ -91,9 +74,6 @@ export function removeFromTaskList(projectRoot: string, taskId: string): boolean
|
|
|
91
74
|
return true;
|
|
92
75
|
}
|
|
93
76
|
|
|
94
|
-
/**
|
|
95
|
-
* List all tasks referenced in tasks.jsonl.
|
|
96
|
-
*/
|
|
97
77
|
export function listTasks(projectRoot: string): ParsedTask[] {
|
|
98
78
|
const listPath = path.join(projectRoot, "tasks.jsonl");
|
|
99
79
|
if (!fs.existsSync(listPath)) return [];
|
|
@@ -118,25 +98,15 @@ export function listTasks(projectRoot: string): ParsedTask[] {
|
|
|
118
98
|
return tasks.reverse();
|
|
119
99
|
}
|
|
120
100
|
|
|
121
|
-
/**
|
|
122
|
-
* Get the directory path for a task by its ID.
|
|
123
|
-
*/
|
|
124
101
|
export function getTaskDir(projectRoot: string, taskId: string): string {
|
|
125
102
|
return path.join(projectRoot, "tasks", taskId);
|
|
126
103
|
}
|
|
127
104
|
|
|
128
|
-
/**
|
|
129
|
-
* Write task status to status.json in the task directory.
|
|
130
|
-
*/
|
|
131
105
|
export function writeTaskStatus(taskDir: string, status: TaskStatus): void {
|
|
132
106
|
const filePath = path.join(taskDir, "status.json");
|
|
133
107
|
fs.writeFileSync(filePath, JSON.stringify(status), "utf-8");
|
|
134
108
|
}
|
|
135
109
|
|
|
136
|
-
/**
|
|
137
|
-
* Read task status from status.json in the task directory.
|
|
138
|
-
* Returns undefined if the file doesn't exist.
|
|
139
|
-
*/
|
|
140
110
|
export function readTaskStatus(taskDir: string): TaskStatus | undefined {
|
|
141
111
|
const filePath = path.join(taskDir, "status.json");
|
|
142
112
|
try {
|
|
@@ -146,10 +116,7 @@ export function readTaskStatus(taskDir: string): TaskStatus | undefined {
|
|
|
146
116
|
}
|
|
147
117
|
}
|
|
148
118
|
|
|
149
|
-
/**
|
|
150
|
-
* Create a run directory with an initial TASKRUN.md file.
|
|
151
|
-
* Returns the run ID (timestamp string used as directory name).
|
|
152
|
-
*/
|
|
119
|
+
/** Returns the run ID (timestamp string used as directory name). */
|
|
153
120
|
export function createRunDir(
|
|
154
121
|
taskDir: string,
|
|
155
122
|
taskName: string,
|
|
@@ -165,16 +132,10 @@ export function createRunDir(
|
|
|
165
132
|
return runId;
|
|
166
133
|
}
|
|
167
134
|
|
|
168
|
-
/**
|
|
169
|
-
* Get the path to a run directory.
|
|
170
|
-
*/
|
|
171
135
|
export function getRunDir(taskDir: string, runId: string): string {
|
|
172
136
|
return path.join(taskDir, runId);
|
|
173
137
|
}
|
|
174
138
|
|
|
175
|
-
/**
|
|
176
|
-
* Append a conversation message to a run's TASKRUN.md file.
|
|
177
|
-
*/
|
|
178
139
|
export function appendRunMessage(
|
|
179
140
|
taskDir: string,
|
|
180
141
|
runId: string,
|
|
@@ -189,10 +150,6 @@ export function appendRunMessage(
|
|
|
189
150
|
fs.appendFileSync(path.join(taskDir, runId, "TASKRUN.md"), entry, "utf-8");
|
|
190
151
|
}
|
|
191
152
|
|
|
192
|
-
/**
|
|
193
|
-
* Begin a streaming assistant message — writes the delimiter only.
|
|
194
|
-
* Returns a writer that appends content chunks and finalizes the message.
|
|
195
|
-
*/
|
|
196
153
|
export function beginStreamingMessage(
|
|
197
154
|
taskDir: string,
|
|
198
155
|
runId: string,
|
|
@@ -217,7 +174,7 @@ export class StreamingMessageWriter {
|
|
|
217
174
|
fs.appendFileSync(this.filePath, "\n\n", "utf-8");
|
|
218
175
|
if (attachments?.length) {
|
|
219
176
|
const raw = fs.readFileSync(this.filePath, "utf-8");
|
|
220
|
-
//
|
|
177
|
+
// spliceUserMessage may have created a newer assistant delimiter.
|
|
221
178
|
const pattern = /<!-- palmier:message role="assistant" time="\d+" -->/g;
|
|
222
179
|
let lastMatch: RegExpExecArray | null = null;
|
|
223
180
|
let m;
|
|
@@ -233,12 +190,10 @@ export class StreamingMessageWriter {
|
|
|
233
190
|
}
|
|
234
191
|
|
|
235
192
|
/**
|
|
236
|
-
* Splice a user message into a running assistant stream
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
* write() is just appendFileSync, so subsequent chunks land in
|
|
241
|
-
* the new assistant block.
|
|
193
|
+
* Splice a user message into a running assistant stream: close the current
|
|
194
|
+
* assistant block, write the user message, open a new assistant block. Direct
|
|
195
|
+
* appends only, so an existing StreamingMessageWriter keeps working — its
|
|
196
|
+
* subsequent chunks land in the new block.
|
|
242
197
|
*/
|
|
243
198
|
export function spliceUserMessage(
|
|
244
199
|
taskDir: string,
|
|
@@ -248,22 +203,15 @@ export function spliceUserMessage(
|
|
|
248
203
|
assistantAppend?: string,
|
|
249
204
|
): void {
|
|
250
205
|
const filePath = path.join(taskDir, runId, "TASKRUN.md");
|
|
251
|
-
// 1. Optionally append to the current assistant block (e.g. the input questions)
|
|
252
206
|
if (assistantAppend) {
|
|
253
207
|
fs.appendFileSync(filePath, assistantAppend, "utf-8");
|
|
254
208
|
}
|
|
255
|
-
// 2. End the current assistant block
|
|
256
209
|
fs.appendFileSync(filePath, "\n\n", "utf-8");
|
|
257
|
-
// 3. Write the user message
|
|
258
210
|
appendRunMessage(taskDir, runId, userMsg);
|
|
259
|
-
// 4. Open a new assistant block for subsequent agent output
|
|
260
211
|
const delimiter = `<!-- palmier:message role="assistant" time="${Date.now()}" -->`;
|
|
261
212
|
fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
|
|
262
213
|
}
|
|
263
214
|
|
|
264
|
-
/**
|
|
265
|
-
* Read conversation messages from a run's TASKRUN.md file.
|
|
266
|
-
*/
|
|
267
215
|
export function readRunMessages(taskDir: string, runId: string): ConversationMessage[] {
|
|
268
216
|
const raw = fs.readFileSync(path.join(taskDir, runId, "TASKRUN.md"), "utf-8");
|
|
269
217
|
const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
@@ -298,18 +246,11 @@ export function readRunMessages(taskDir: string, runId: string): ConversationMes
|
|
|
298
246
|
return messages;
|
|
299
247
|
}
|
|
300
248
|
|
|
301
|
-
/**
|
|
302
|
-
* Append a history entry to the project-level history.jsonl file.
|
|
303
|
-
*/
|
|
304
249
|
export function appendHistory(projectRoot: string, entry: HistoryEntry): void {
|
|
305
250
|
const historyPath = path.join(projectRoot, "history.jsonl");
|
|
306
251
|
fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
307
252
|
}
|
|
308
253
|
|
|
309
|
-
/**
|
|
310
|
-
* Delete a history entry and its associated run directory.
|
|
311
|
-
* Returns true if the entry was found and removed.
|
|
312
|
-
*/
|
|
313
254
|
export function deleteHistoryEntry(
|
|
314
255
|
projectRoot: string,
|
|
315
256
|
taskId: string,
|
|
@@ -337,7 +278,6 @@ export function deleteHistoryEntry(
|
|
|
337
278
|
|
|
338
279
|
fs.writeFileSync(historyPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
|
|
339
280
|
|
|
340
|
-
// Delete the run directory
|
|
341
281
|
const runDir = path.join(projectRoot, "tasks", taskId, runId);
|
|
342
282
|
if (fs.existsSync(runDir)) {
|
|
343
283
|
fs.rmSync(runDir, { recursive: true, force: true });
|
|
@@ -346,10 +286,7 @@ export function deleteHistoryEntry(
|
|
|
346
286
|
return true;
|
|
347
287
|
}
|
|
348
288
|
|
|
349
|
-
/**
|
|
350
|
-
* Read history entries from history.jsonl with pagination.
|
|
351
|
-
* Returns entries sorted most-recent-first.
|
|
352
|
-
*/
|
|
289
|
+
/** Returns entries most-recent-first. */
|
|
353
290
|
export function readHistory(
|
|
354
291
|
projectRoot: string,
|
|
355
292
|
opts: { offset?: number; limit?: number; task_id?: string },
|