palmier 0.6.0 → 0.6.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/.github/workflows/publish.yml +15 -2
- package/CLAUDE.md +2 -2
- package/DISCLAIMER.md +36 -0
- package/README.md +76 -87
- package/dist/agents/agent-instructions.md +1 -1
- package/dist/agents/agent.d.ts +2 -0
- package/dist/agents/agent.js +21 -0
- package/dist/agents/aider.d.ts +9 -0
- package/dist/agents/aider.js +32 -0
- package/dist/agents/cursor.d.ts +9 -0
- package/dist/agents/cursor.js +35 -0
- package/dist/agents/deepagents.d.ts +9 -0
- package/dist/agents/deepagents.js +35 -0
- package/dist/agents/droid.d.ts +9 -0
- package/dist/agents/droid.js +32 -0
- package/dist/agents/goose.d.ts +9 -0
- package/dist/agents/goose.js +32 -0
- package/dist/agents/opencode.d.ts +9 -0
- package/dist/agents/opencode.js +35 -0
- package/dist/agents/openhands.d.ts +9 -0
- package/dist/agents/openhands.js +35 -0
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +1 -1
- package/dist/commands/run.js +2 -2
- package/dist/pwa/apple-touch-icon.png +0 -0
- package/dist/pwa/assets/index-ByhOhTz1.js +118 -0
- package/dist/pwa/assets/index-_AmC1Rkn.css +1 -0
- package/dist/pwa/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
- package/dist/pwa/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
- package/dist/pwa/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
- package/dist/pwa/favicon.ico +0 -0
- package/dist/pwa/index.html +17 -0
- package/dist/pwa/manifest.webmanifest +1 -0
- package/dist/pwa/pwa-192x192.png +0 -0
- package/dist/pwa/pwa-512x512.png +0 -0
- package/dist/pwa/registerSW.js +1 -0
- package/dist/pwa/service-worker.js +2 -0
- package/dist/rpc-handler.d.ts +4 -0
- package/dist/rpc-handler.js +5 -4
- package/dist/transports/http-transport.js +29 -41
- package/package.json +2 -2
- package/palmier-server/.github/workflows/ci.yml +21 -0
- package/palmier-server/.github/workflows/deploy.yml +38 -0
- package/palmier-server/CLAUDE.md +13 -0
- package/palmier-server/PRODUCTION.md +355 -0
- package/palmier-server/README.md +187 -0
- package/palmier-server/nats.conf +15 -0
- package/palmier-server/package.json +8 -0
- package/palmier-server/pnpm-lock.yaml +6597 -0
- package/palmier-server/pnpm-workspace.yaml +3 -0
- package/palmier-server/pwa/index.html +16 -0
- package/palmier-server/pwa/logo/logo-prompt.md +28 -0
- package/palmier-server/pwa/logo/logo_20260330.png +0 -0
- package/palmier-server/pwa/package.json +30 -0
- package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
- package/palmier-server/pwa/public/favicon.ico +0 -0
- package/palmier-server/pwa/public/pwa-192x192.png +0 -0
- package/palmier-server/pwa/public/pwa-512x512.png +0 -0
- package/palmier-server/pwa/src/App.css +2387 -0
- package/palmier-server/pwa/src/App.tsx +21 -0
- package/palmier-server/pwa/src/agentLabels.ts +11 -0
- package/palmier-server/pwa/src/api.ts +61 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +289 -0
- package/palmier-server/pwa/src/components/PlanDialog.tsx +41 -0
- package/palmier-server/pwa/src/components/RunDetailView.tsx +293 -0
- package/palmier-server/pwa/src/components/RunsView.tsx +254 -0
- package/palmier-server/pwa/src/components/TabBar.tsx +31 -0
- package/palmier-server/pwa/src/components/TaskCard.tsx +213 -0
- package/palmier-server/pwa/src/components/TaskForm.tsx +580 -0
- package/palmier-server/pwa/src/components/TaskListView.tsx +415 -0
- package/palmier-server/pwa/src/constants.ts +2 -0
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +313 -0
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +135 -0
- package/palmier-server/pwa/src/formatTime.ts +10 -0
- package/palmier-server/pwa/src/hooks/useBackClose.ts +75 -0
- package/palmier-server/pwa/src/hooks/useMediaQuery.ts +17 -0
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +75 -0
- package/palmier-server/pwa/src/main.tsx +14 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +223 -0
- package/palmier-server/pwa/src/pages/PairHost.tsx +178 -0
- package/palmier-server/pwa/src/service-worker.ts +139 -0
- package/palmier-server/pwa/src/types.ts +79 -0
- package/palmier-server/pwa/src/vite-env.d.ts +11 -0
- package/palmier-server/pwa/tsconfig.json +21 -0
- package/palmier-server/pwa/tsconfig.node.json +19 -0
- package/palmier-server/pwa/vite.config.ts +47 -0
- package/palmier-server/server/.env.example +16 -0
- package/palmier-server/server/package.json +33 -0
- package/palmier-server/server/src/db.ts +34 -0
- package/palmier-server/server/src/index.ts +219 -0
- package/palmier-server/server/src/nats.ts +25 -0
- package/palmier-server/server/src/push.ts +68 -0
- package/palmier-server/server/src/routes/hosts.ts +45 -0
- package/palmier-server/server/src/routes/push.ts +100 -0
- package/palmier-server/server/tsconfig.json +20 -0
- package/palmier-server/spec.md +415 -0
- package/src/agents/agent-instructions.md +1 -1
- package/src/agents/agent.ts +23 -0
- package/src/agents/aider.ts +37 -0
- package/src/agents/cursor.ts +38 -0
- package/src/agents/deepagents.ts +38 -0
- package/src/agents/droid.ts +37 -0
- package/src/agents/goose.ts +35 -0
- package/src/agents/opencode.ts +38 -0
- package/src/agents/openhands.ts +38 -0
- package/src/commands/pair.ts +1 -1
- package/src/commands/run.ts +2 -2
- package/src/rpc-handler.ts +5 -4
- package/src/transports/http-transport.ts +31 -43
- package/test/result-state.test.ts +110 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
|
+
import { SHELL } from "../platform/index.js";
|
|
6
|
+
|
|
7
|
+
export class GooseAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
9
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
10
|
+
return {
|
|
11
|
+
command: "goose",
|
|
12
|
+
args: ["run", "--text", prompt],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = ["run"];
|
|
20
|
+
|
|
21
|
+
if (followupPrompt) {args.push("--resume");} // continue mode for followups
|
|
22
|
+
args.push("--text", prompt);
|
|
23
|
+
|
|
24
|
+
return { command: "goose", args, ...(yolo ? { env: { GOOSE_MODE: "auto" } } : {}) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async init(): Promise<boolean> {
|
|
28
|
+
try {
|
|
29
|
+
execSync("goose --version", { stdio: "ignore", shell: SHELL });
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
|
+
import { SHELL } from "../platform/index.js";
|
|
6
|
+
|
|
7
|
+
export class OpenCodeAgent implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
9
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
10
|
+
return {
|
|
11
|
+
command: "opencode",
|
|
12
|
+
args: ["run", prompt],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = ["run"];
|
|
20
|
+
|
|
21
|
+
if (yolo) {
|
|
22
|
+
args.push("--dangerously-skip-permissions");
|
|
23
|
+
}
|
|
24
|
+
if (followupPrompt) {args.push("--continue");} // continue mode for followups
|
|
25
|
+
args.push(prompt);
|
|
26
|
+
|
|
27
|
+
return { command: "opencode", args};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async init(): Promise<boolean> {
|
|
31
|
+
try {
|
|
32
|
+
execSync("opencode --version", { stdio: "ignore", shell: SHELL });
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import type { AgentTool, CommandLine } from "./agent.js";
|
|
4
|
+
import { getAgentInstructions } from "./shared-prompt.js";
|
|
5
|
+
import { SHELL } from "../platform/index.js";
|
|
6
|
+
|
|
7
|
+
export class OpenHands implements AgentTool {
|
|
8
|
+
supportsPermissions = false;
|
|
9
|
+
getPlanGenerationCommandLine(prompt: string): CommandLine {
|
|
10
|
+
return {
|
|
11
|
+
command: "openhands",
|
|
12
|
+
args: ["--headless", "-t", prompt],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
|
|
17
|
+
const yolo = extraPermissions === "yolo";
|
|
18
|
+
const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
|
|
19
|
+
const args = ["--headless"];
|
|
20
|
+
|
|
21
|
+
if (yolo) {
|
|
22
|
+
args.push("--always-approve");
|
|
23
|
+
}
|
|
24
|
+
if (followupPrompt) {args.push("--resume", "--last");} // continue mode for followups
|
|
25
|
+
args.push("-t", prompt);
|
|
26
|
+
|
|
27
|
+
return { command: "openhands", args};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async init(): Promise<boolean> {
|
|
31
|
+
try {
|
|
32
|
+
execSync("openhands --version", { stdio: "ignore", shell: SHELL });
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/commands/pair.ts
CHANGED
|
@@ -61,7 +61,7 @@ function httpPairRegister(port: number, code: string): Promise<boolean> {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Generate
|
|
64
|
+
* Generate a pairing code and wait for a PWA client to pair.
|
|
65
65
|
* Listens on NATS (server mode) and HTTP (via serve daemon) in parallel.
|
|
66
66
|
*/
|
|
67
67
|
export async function pairCommand(): Promise<void> {
|
package/src/commands/run.ts
CHANGED
|
@@ -62,7 +62,7 @@ async function invokeAgentWithRetries(
|
|
|
62
62
|
}, 500);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(
|
|
65
|
+
const { command, args, stdin, env: agentEnv } = ctx.agent.getTaskRunCommandLine(
|
|
66
66
|
invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions,
|
|
67
67
|
);
|
|
68
68
|
const truncate = (s: string, max = 100) => s.length > max ? s.slice(0, max) + "…" : s;
|
|
@@ -70,7 +70,7 @@ async function invokeAgentWithRetries(
|
|
|
70
70
|
console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
|
|
71
71
|
const result = await spawnCommand(command, args, {
|
|
72
72
|
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
73
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
|
|
73
|
+
env: { ...ctx.guiEnv, ...agentEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
|
|
74
74
|
echoStdout: true,
|
|
75
75
|
resolveOnFailure: true,
|
|
76
76
|
stdin,
|
package/src/rpc-handler.ts
CHANGED
|
@@ -27,7 +27,7 @@ const PLAN_GENERATION_PROMPT = fs.readFileSync(
|
|
|
27
27
|
/**
|
|
28
28
|
* Parse RESULT frontmatter and conversation messages.
|
|
29
29
|
*/
|
|
30
|
-
function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
30
|
+
export function parseResultFrontmatter(raw: string): Record<string, unknown> {
|
|
31
31
|
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
32
32
|
if (!fmMatch) return { messages: [] };
|
|
33
33
|
|
|
@@ -124,13 +124,14 @@ async function generatePlan(
|
|
|
124
124
|
): Promise<{ name: string; body: string }> {
|
|
125
125
|
const fullPrompt = PLAN_GENERATION_PROMPT + userPrompt;
|
|
126
126
|
const planAgent = getAgent(agentName);
|
|
127
|
-
const { command, args, stdin } = planAgent.getPlanGenerationCommandLine(fullPrompt);
|
|
127
|
+
const { command, args, stdin, env: agentEnv } = planAgent.getPlanGenerationCommandLine(fullPrompt);
|
|
128
128
|
console.log(`[generatePlan] Running: ${command} ${args.join(" ")}`);
|
|
129
129
|
|
|
130
130
|
const { output } = await spawnCommand(command, args, {
|
|
131
131
|
cwd: projectRoot,
|
|
132
132
|
timeout: 120_000,
|
|
133
133
|
stdin,
|
|
134
|
+
...(agentEnv ? { env: agentEnv } : {}),
|
|
134
135
|
});
|
|
135
136
|
|
|
136
137
|
let name = "";
|
|
@@ -423,7 +424,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
423
424
|
|
|
424
425
|
// Fire-and-forget: invoke agent inline as a child of the serve process
|
|
425
426
|
const followupAgent = getAgent(followupTask.frontmatter.agent);
|
|
426
|
-
const { command: cmd, args: cmdArgs, stdin } = followupAgent.getTaskRunCommandLine(
|
|
427
|
+
const { command: cmd, args: cmdArgs, stdin, env: followupAgentEnv } = followupAgent.getTaskRunCommandLine(
|
|
427
428
|
followupTask, params.message, followupTask.frontmatter.yolo_mode ? "yolo" : followupTask.frontmatter.permissions,
|
|
428
429
|
);
|
|
429
430
|
|
|
@@ -431,7 +432,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
431
432
|
const child = crossSpawn(cmd, cmdArgs, {
|
|
432
433
|
cwd: followupRunDir,
|
|
433
434
|
stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
434
|
-
env: { ...process.env, PALMIER_TASK_ID: params.id },
|
|
435
|
+
env: { ...process.env, ...followupAgentEnv, PALMIER_TASK_ID: params.id },
|
|
435
436
|
windowsHide: true,
|
|
436
437
|
});
|
|
437
438
|
if (stdin != null) child.stdin!.end(stdin);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
2
|
import * as os from "os";
|
|
3
|
+
import * as path from "node:path";
|
|
3
4
|
import { StringCodec, type NatsConnection } from "nats";
|
|
4
5
|
import { validateClient, addClient } from "../client-store.js";
|
|
5
6
|
import { registerPending } from "../pending-requests.js";
|
|
@@ -7,9 +8,7 @@ import * as fs from "node:fs";
|
|
|
7
8
|
import { getTaskDir, parseTaskFile, spliceUserMessage } from "../task.js";
|
|
8
9
|
import type { HostConfig, RpcMessage, RequiredPermission } from "../types.js";
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// ── On-the-fly PWA asset cache ──────────────────────────────────────────
|
|
11
|
+
// ── Bundled PWA asset serving ───────────────────────────────────────────
|
|
13
12
|
|
|
14
13
|
interface CachedAsset {
|
|
15
14
|
data: Buffer;
|
|
@@ -17,8 +16,8 @@ interface CachedAsset {
|
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
const assetCache = new Map<string, CachedAsset>();
|
|
20
|
-
|
|
21
|
-
const
|
|
19
|
+
|
|
20
|
+
const PWA_DIR = path.join(import.meta.dirname, "..", "pwa");
|
|
22
21
|
|
|
23
22
|
const CONTENT_TYPES: Record<string, string> = {
|
|
24
23
|
".html": "text/html; charset=utf-8",
|
|
@@ -30,6 +29,7 @@ const CONTENT_TYPES: Record<string, string> = {
|
|
|
30
29
|
".woff2": "font/woff2",
|
|
31
30
|
".woff": "font/woff",
|
|
32
31
|
".svg": "image/svg+xml",
|
|
32
|
+
".webmanifest": "application/manifest+json",
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
function guessContentType(urlPath: string): string {
|
|
@@ -38,45 +38,32 @@ function guessContentType(urlPath: string): string {
|
|
|
38
38
|
return CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
async function fetchBuffer(url: string): Promise<Buffer> {
|
|
42
|
-
const res = await fetch(url);
|
|
43
|
-
if (!res.ok) throw new Error(`${res.status} ${res.statusText} for ${url}`);
|
|
44
|
-
return Buffer.from(await res.arrayBuffer());
|
|
45
|
-
}
|
|
46
|
-
|
|
47
41
|
/**
|
|
48
|
-
*
|
|
49
|
-
* Returns null if the
|
|
42
|
+
* Read a PWA asset from the bundled pwa/ directory, caching in memory.
|
|
43
|
+
* Returns null if the file does not exist.
|
|
50
44
|
*/
|
|
51
|
-
|
|
45
|
+
function getAsset(urlPath: string): CachedAsset | null {
|
|
52
46
|
const cached = assetCache.get(urlPath);
|
|
53
47
|
if (cached) return cached;
|
|
54
48
|
|
|
55
|
-
|
|
56
|
-
const inflight = assetInflight.get(urlPath);
|
|
57
|
-
if (inflight) return inflight;
|
|
58
|
-
|
|
59
|
-
const promise = (async () => {
|
|
60
|
-
try {
|
|
61
|
-
let data = await fetchBuffer(`${PWA_ORIGIN}${urlPath}`);
|
|
62
|
-
// Inject LAN mode marker into index HTML so the PWA can detect it's served by palmier
|
|
63
|
-
if (urlPath === "/") {
|
|
64
|
-
const html = data.toString("utf-8").replace("</head>", "<script>window.__PALMIER_SERVE__=true</script></head>");
|
|
65
|
-
data = Buffer.from(html, "utf-8");
|
|
66
|
-
}
|
|
67
|
-
const asset: CachedAsset = { data, contentType: guessContentType(urlPath) };
|
|
68
|
-
assetCache.set(urlPath, asset);
|
|
69
|
-
return asset;
|
|
70
|
-
} catch (err) {
|
|
71
|
-
console.warn(`[pwa] Failed to fetch ${urlPath}: ${err}`);
|
|
72
|
-
return null;
|
|
73
|
-
} finally {
|
|
74
|
-
assetInflight.delete(urlPath);
|
|
75
|
-
}
|
|
76
|
-
})();
|
|
49
|
+
const filePath = path.join(PWA_DIR, urlPath === "/" ? "index.html" : urlPath);
|
|
77
50
|
|
|
78
|
-
|
|
79
|
-
return
|
|
51
|
+
// Prevent path traversal
|
|
52
|
+
if (!filePath.startsWith(PWA_DIR)) return null;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
let data = fs.readFileSync(filePath);
|
|
56
|
+
// Inject marker into index HTML so the PWA can detect it's served by palmier
|
|
57
|
+
if (urlPath === "/") {
|
|
58
|
+
const html = data.toString("utf-8").replace("</head>", "<script>window.__PALMIER_SERVE__=true</script></head>");
|
|
59
|
+
data = Buffer.from(html, "utf-8");
|
|
60
|
+
}
|
|
61
|
+
const asset: CachedAsset = { data, contentType: guessContentType(urlPath) };
|
|
62
|
+
assetCache.set(urlPath, asset);
|
|
63
|
+
return asset;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
80
67
|
}
|
|
81
68
|
|
|
82
69
|
type SseClient = http.ServerResponse;
|
|
@@ -238,11 +225,12 @@ export async function startHttpTransport(
|
|
|
238
225
|
|
|
239
226
|
try {
|
|
240
227
|
const body = await readBody(req);
|
|
241
|
-
const { title, body: notifBody } = JSON.parse(body) as { title: string; body: string };
|
|
228
|
+
const { taskId: notifTaskId, title, body: notifBody } = JSON.parse(body) as { taskId?: string; title: string; body: string };
|
|
242
229
|
if (!title || !notifBody) { sendJson(res, 400, { error: "title and body are required" }); return; }
|
|
243
230
|
|
|
244
231
|
const sc = StringCodec();
|
|
245
|
-
const payload = { hostId: config.hostId, title, body: notifBody };
|
|
232
|
+
const payload: Record<string, string> = { hostId: config.hostId, title, body: notifBody };
|
|
233
|
+
if (notifTaskId) payload.task_id = notifTaskId;
|
|
246
234
|
const subject = `host.${config.hostId}.push.send`;
|
|
247
235
|
const reply = await nc.request(subject, sc.encode(JSON.stringify(payload)), { timeout: 15_000 });
|
|
248
236
|
const result = JSON.parse(sc.decode(reply.data)) as { ok?: boolean; error?: string };
|
|
@@ -383,7 +371,7 @@ export async function startHttpTransport(
|
|
|
383
371
|
return;
|
|
384
372
|
}
|
|
385
373
|
|
|
386
|
-
// ── Public pair endpoint — no auth, PWA posts
|
|
374
|
+
// ── Public pair endpoint — no auth, PWA posts pairing code here ────────
|
|
387
375
|
|
|
388
376
|
if (req.method === "POST" && pathname === "/pair") {
|
|
389
377
|
try {
|
|
@@ -421,9 +409,9 @@ export async function startHttpTransport(
|
|
|
421
409
|
if (SKIP.has(pathname)) { sendJson(res, 404, { error: "Not found" }); return; }
|
|
422
410
|
|
|
423
411
|
// Try exact path, then fall back to index.html (SPA routing)
|
|
424
|
-
let asset =
|
|
412
|
+
let asset = getAsset(pathname);
|
|
425
413
|
if (!asset && pathname !== "/") {
|
|
426
|
-
asset =
|
|
414
|
+
asset = getAsset("/");
|
|
427
415
|
}
|
|
428
416
|
|
|
429
417
|
if (asset) {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import {
|
|
7
|
+
createRunDir,
|
|
8
|
+
appendRunMessage,
|
|
9
|
+
beginStreamingMessage,
|
|
10
|
+
} from "../src/task.js";
|
|
11
|
+
import { parseResultFrontmatter } from "../src/rpc-handler.js";
|
|
12
|
+
|
|
13
|
+
let taskDir: string;
|
|
14
|
+
let runId: string;
|
|
15
|
+
|
|
16
|
+
function setup() {
|
|
17
|
+
taskDir = fs.mkdtempSync(path.join(os.tmpdir(), "palmier-test-"));
|
|
18
|
+
runId = createRunDir(taskDir, "Test Task", 1000, "claude");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readRaw(): string {
|
|
22
|
+
return fs.readFileSync(path.join(taskDir, runId, "TASKRUN.md"), "utf-8");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("parseResultFrontmatter — monitoring state", () => {
|
|
26
|
+
beforeEach(setup);
|
|
27
|
+
|
|
28
|
+
it("returns 'monitoring' when monitoring is the last message", () => {
|
|
29
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1000, content: "", type: "started" });
|
|
30
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1001, content: "", type: "monitoring" });
|
|
31
|
+
|
|
32
|
+
const result = parseResultFrontmatter(readRaw());
|
|
33
|
+
assert.equal(result.running_state, "monitoring");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns 'started' when an assistant message follows monitoring", () => {
|
|
37
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1000, content: "", type: "started" });
|
|
38
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1001, content: "", type: "monitoring" });
|
|
39
|
+
const writer = beginStreamingMessage(taskDir, runId, 1002);
|
|
40
|
+
writer.write("Working on it...");
|
|
41
|
+
writer.end();
|
|
42
|
+
|
|
43
|
+
const result = parseResultFrontmatter(readRaw());
|
|
44
|
+
assert.equal(result.running_state, "started");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns 'monitoring' after agent finishes and monitoring resumes", () => {
|
|
48
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1000, content: "", type: "started" });
|
|
49
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1001, content: "", type: "monitoring" });
|
|
50
|
+
// Agent processes a line
|
|
51
|
+
const writer = beginStreamingMessage(taskDir, runId, 1002);
|
|
52
|
+
writer.write("Done processing line.");
|
|
53
|
+
writer.end();
|
|
54
|
+
// Back to monitoring
|
|
55
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1003, content: "", type: "monitoring" });
|
|
56
|
+
|
|
57
|
+
const result = parseResultFrontmatter(readRaw());
|
|
58
|
+
assert.equal(result.running_state, "monitoring");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns 'started' when a user message follows monitoring", () => {
|
|
62
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1000, content: "", type: "started" });
|
|
63
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1001, content: "", type: "monitoring" });
|
|
64
|
+
appendRunMessage(taskDir, runId, { role: "user", time: 1002, content: "some input" });
|
|
65
|
+
|
|
66
|
+
const result = parseResultFrontmatter(readRaw());
|
|
67
|
+
assert.equal(result.running_state, "started");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("parseResultFrontmatter — standard states", () => {
|
|
72
|
+
beforeEach(setup);
|
|
73
|
+
|
|
74
|
+
it("returns 'started' for a running task", () => {
|
|
75
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1000, content: "", type: "started" });
|
|
76
|
+
appendRunMessage(taskDir, runId, { role: "user", time: 1001, content: "Do something" });
|
|
77
|
+
|
|
78
|
+
const result = parseResultFrontmatter(readRaw());
|
|
79
|
+
assert.equal(result.running_state, "started");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns 'finished' for a completed task", () => {
|
|
83
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1000, content: "", type: "started" });
|
|
84
|
+
appendRunMessage(taskDir, runId, { role: "user", time: 1001, content: "Do something" });
|
|
85
|
+
const writer = beginStreamingMessage(taskDir, runId, 1002);
|
|
86
|
+
writer.write("Done.");
|
|
87
|
+
writer.end();
|
|
88
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1003, content: "", type: "finished" });
|
|
89
|
+
|
|
90
|
+
const result = parseResultFrontmatter(readRaw());
|
|
91
|
+
assert.equal(result.running_state, "finished");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns 'failed' for a failed task", () => {
|
|
95
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1000, content: "", type: "started" });
|
|
96
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1001, content: "", type: "failed" });
|
|
97
|
+
|
|
98
|
+
const result = parseResultFrontmatter(readRaw());
|
|
99
|
+
assert.equal(result.running_state, "failed");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns 'followup' when started again after terminal state", () => {
|
|
103
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1000, content: "", type: "started" });
|
|
104
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1001, content: "", type: "finished" });
|
|
105
|
+
appendRunMessage(taskDir, runId, { role: "status", time: 1002, content: "", type: "started" });
|
|
106
|
+
|
|
107
|
+
const result = parseResultFrontmatter(readRaw());
|
|
108
|
+
assert.equal(result.running_state, "followup");
|
|
109
|
+
});
|
|
110
|
+
});
|