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.
Files changed (79) hide show
  1. package/CLAUDE.md +5 -1
  2. package/README.md +135 -45
  3. package/dist/agents/agent.d.ts +26 -0
  4. package/dist/agents/agent.js +32 -0
  5. package/dist/agents/claude.d.ts +8 -0
  6. package/dist/agents/claude.js +35 -0
  7. package/dist/agents/codex.d.ts +8 -0
  8. package/dist/agents/codex.js +41 -0
  9. package/dist/agents/gemini.d.ts +8 -0
  10. package/dist/agents/gemini.js +39 -0
  11. package/dist/agents/openclaw.d.ts +8 -0
  12. package/dist/agents/openclaw.js +25 -0
  13. package/dist/agents/shared-prompt.d.ts +11 -0
  14. package/dist/agents/shared-prompt.js +26 -0
  15. package/dist/commands/agents.d.ts +2 -0
  16. package/dist/commands/agents.js +19 -0
  17. package/dist/commands/info.d.ts +5 -0
  18. package/dist/commands/info.js +40 -0
  19. package/dist/commands/init.d.ts +7 -2
  20. package/dist/commands/init.js +139 -49
  21. package/dist/commands/mcpserver.d.ts +2 -0
  22. package/dist/commands/mcpserver.js +75 -0
  23. package/dist/commands/pair.d.ts +6 -0
  24. package/dist/commands/pair.js +140 -0
  25. package/dist/commands/plan-generation.md +32 -0
  26. package/dist/commands/run.d.ts +0 -1
  27. package/dist/commands/run.js +258 -114
  28. package/dist/commands/serve.d.ts +1 -1
  29. package/dist/commands/serve.js +16 -228
  30. package/dist/commands/sessions.d.ts +4 -0
  31. package/dist/commands/sessions.js +30 -0
  32. package/dist/commands/task-generation.md +1 -1
  33. package/dist/config.d.ts +5 -5
  34. package/dist/config.js +24 -6
  35. package/dist/index.js +58 -5
  36. package/dist/nats-client.d.ts +3 -3
  37. package/dist/nats-client.js +2 -2
  38. package/dist/rpc-handler.d.ts +6 -0
  39. package/dist/rpc-handler.js +367 -0
  40. package/dist/session-store.d.ts +12 -0
  41. package/dist/session-store.js +57 -0
  42. package/dist/spawn-command.d.ts +26 -0
  43. package/dist/spawn-command.js +48 -0
  44. package/dist/systemd.d.ts +2 -2
  45. package/dist/task.d.ts +45 -2
  46. package/dist/task.js +155 -14
  47. package/dist/transports/http-transport.d.ts +6 -0
  48. package/dist/transports/http-transport.js +243 -0
  49. package/dist/transports/nats-transport.d.ts +6 -0
  50. package/dist/transports/nats-transport.js +69 -0
  51. package/dist/types.d.ts +30 -13
  52. package/package.json +4 -3
  53. package/src/agents/agent.ts +62 -0
  54. package/src/agents/claude.ts +39 -0
  55. package/src/agents/codex.ts +46 -0
  56. package/src/agents/gemini.ts +43 -0
  57. package/src/agents/openclaw.ts +29 -0
  58. package/src/agents/shared-prompt.ts +26 -0
  59. package/src/commands/agents.ts +20 -0
  60. package/src/commands/info.ts +44 -0
  61. package/src/commands/init.ts +229 -121
  62. package/src/commands/mcpserver.ts +92 -0
  63. package/src/commands/pair.ts +163 -0
  64. package/src/commands/plan-generation.md +32 -0
  65. package/src/commands/run.ts +323 -129
  66. package/src/commands/serve.ts +26 -287
  67. package/src/commands/sessions.ts +32 -0
  68. package/src/config.ts +30 -10
  69. package/src/index.ts +67 -6
  70. package/src/nats-client.ts +4 -4
  71. package/src/rpc-handler.ts +421 -0
  72. package/src/session-store.ts +68 -0
  73. package/src/spawn-command.ts +78 -0
  74. package/src/systemd.ts +2 -2
  75. package/src/task.ts +166 -16
  76. package/src/transports/http-transport.ts +290 -0
  77. package/src/transports/nats-transport.ts +82 -0
  78. package/src/types.ts +36 -13
  79. package/src/commands/task-generation.md +0 -28
@@ -1,92 +1,182 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
+ import * as os from "os";
4
+ import * as readline from "readline";
5
+ import { randomUUID, randomBytes } from "crypto";
3
6
  import { execSync } from "child_process";
4
7
  import { homedir } from "os";
5
8
  import { saveConfig } from "../config.js";
9
+ import { detectAgents } from "../agents/agent.js";
10
+ import { pairCommand } from "./pair.js";
6
11
  /**
7
- * Provision this agent by exchanging a base64 provisioning token for permanent credentials.
12
+ * Provision this host. Two flows:
13
+ * - palmier init --lan → standalone LAN mode, no server needed
14
+ * - palmier init --server <url> → register with server, prompts for nats/auto mode
8
15
  */
9
16
  export async function initCommand(options) {
10
- // 1. Decode base64 provisioning token
11
- let decoded;
12
- try {
13
- const jsonStr = Buffer.from(options.token, "base64").toString("utf-8");
14
- decoded = JSON.parse(jsonStr);
17
+ if (options.lan) {
18
+ await initLan(options);
15
19
  }
16
- catch {
17
- console.error("Failed to decode provisioning token. Ensure it is a valid base64-encoded JSON string.");
18
- process.exit(1);
20
+ else if (options.server) {
21
+ await initWithServer(options);
19
22
  }
20
- if (!decoded.server || !decoded.token) {
21
- console.error("Invalid provisioning token: missing 'server' or 'token' field.");
23
+ else {
24
+ console.error("Either --lan or --server <url> is required.");
22
25
  process.exit(1);
23
26
  }
24
- console.log(`Claiming agent at ${decoded.server}...`);
25
- // 2. POST to server to claim agent
26
- let claimResponse;
27
+ }
28
+ /**
29
+ * Flow A: Standalone LAN mode. No web server needed.
30
+ */
31
+ async function initLan(options) {
32
+ const hostId = randomUUID();
33
+ const directToken = randomBytes(32).toString("hex");
34
+ const directPort = options.port ?? 7400;
35
+ const lanIp = options.host ?? detectLanIp();
36
+ const config = {
37
+ hostId,
38
+ mode: "lan",
39
+ directPort,
40
+ directToken,
41
+ projectRoot: process.cwd(),
42
+ };
43
+ console.log("Detecting installed agents...");
44
+ config.agents = await detectAgents();
45
+ saveConfig(config);
46
+ console.log(`Host provisioned (LAN mode). ID: ${hostId}`);
47
+ console.log("Config saved to ~/.config/palmier/host.json");
48
+ installSystemdService(config);
49
+ // Auto-enter pair mode so user can connect their PWA immediately
50
+ console.log("");
51
+ console.log("Starting pairing...");
52
+ await pairCommand();
53
+ }
54
+ /**
55
+ * Flow B: Register directly with server. No provisioning token needed.
56
+ */
57
+ async function initWithServer(options) {
58
+ const serverUrl = options.server;
59
+ console.log(`Registering host at ${serverUrl}...`);
60
+ let registerResponse;
27
61
  try {
28
- const res = await fetch(`${decoded.server}/api/agents/claim`, {
62
+ const res = await fetch(`${serverUrl}/api/hosts/register`, {
29
63
  method: "POST",
30
64
  headers: { "Content-Type": "application/json" },
31
- body: JSON.stringify({ provisioning_token: options.token }),
65
+ body: JSON.stringify({}),
32
66
  });
33
67
  if (!res.ok) {
34
68
  const body = await res.text();
35
- console.error(`Failed to claim agent: ${res.status} ${res.statusText}\n${body}`);
69
+ console.error(`Failed to register host: ${res.status} ${res.statusText}\n${body}`);
36
70
  process.exit(1);
37
71
  }
38
- claimResponse = (await res.json());
72
+ registerResponse = (await res.json());
39
73
  }
40
74
  catch (err) {
41
75
  console.error(`Failed to reach server: ${err}`);
42
76
  process.exit(1);
43
77
  }
44
- // 3. Save config
78
+ // Prompt for connection mode
79
+ const mode = await promptMode();
80
+ // Build config
45
81
  const config = {
46
- agentId: claimResponse.agentId,
47
- userId: claimResponse.userId,
48
- natsUrl: claimResponse.natsUrl,
49
- natsWsUrl: claimResponse.natsWsUrl,
50
- natsToken: claimResponse.natsToken,
82
+ hostId: registerResponse.hostId,
51
83
  projectRoot: process.cwd(),
84
+ mode,
85
+ natsUrl: registerResponse.natsUrl,
86
+ natsWsUrl: registerResponse.natsWsUrl,
87
+ natsToken: registerResponse.natsToken,
52
88
  };
89
+ if (mode === "auto") {
90
+ const directToken = randomBytes(32).toString("hex");
91
+ const directPort = options.port ?? 7400;
92
+ const lanIp = options.host ?? detectLanIp();
93
+ config.directPort = directPort;
94
+ config.directToken = directToken;
95
+ console.log("");
96
+ console.log("Direct connection info (for LAN clients):");
97
+ console.log(` Address: ${lanIp}:${directPort}`);
98
+ console.log(` Token: ${directToken}`);
99
+ }
100
+ console.log("Detecting installed agents...");
101
+ config.agents = await detectAgents();
53
102
  saveConfig(config);
54
- console.log(`Agent provisioned. ID: ${config.agentId}`);
55
- console.log("Config saved to ~/.config/palmier/agent.json");
56
- // 4. Install systemd user service for palmier serve
103
+ console.log(`Host provisioned (${mode} mode). ID: ${config.hostId}`);
104
+ console.log("Config saved to ~/.config/palmier/host.json");
105
+ installSystemdService(config);
106
+ // Auto-enter pair mode so user can connect their PWA immediately
107
+ console.log("");
108
+ console.log("Starting pairing...");
109
+ await pairCommand();
110
+ }
111
+ /**
112
+ * Prompt user to select connection mode.
113
+ */
114
+ async function promptMode() {
115
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
116
+ const question = (q) => new Promise((resolve) => rl.question(q, resolve));
117
+ console.log("");
118
+ console.log("Connection mode:");
119
+ console.log(" 1) auto — Both LAN and NATS (recommended)");
120
+ console.log(" 2) nats — NATS only");
121
+ console.log("");
122
+ const answer = await question("Select [1]: ");
123
+ rl.close();
124
+ if (answer.trim() === "2" || answer.trim().toLowerCase() === "nats") {
125
+ return "nats";
126
+ }
127
+ return "auto";
128
+ }
129
+ /**
130
+ * Detect the first non-internal IPv4 address.
131
+ */
132
+ function detectLanIp() {
133
+ const interfaces = os.networkInterfaces();
134
+ for (const name of Object.keys(interfaces)) {
135
+ for (const iface of interfaces[name] ?? []) {
136
+ if (iface.family === "IPv4" && !iface.internal) {
137
+ return iface.address;
138
+ }
139
+ }
140
+ }
141
+ return "127.0.0.1";
142
+ }
143
+ /**
144
+ * Install systemd user service for palmier serve.
145
+ */
146
+ function installSystemdService(config) {
57
147
  const unitDir = path.join(homedir(), ".config", "systemd", "user");
58
148
  fs.mkdirSync(unitDir, { recursive: true });
59
149
  const palmierBin = process.argv[1] || "palmier";
60
- const serviceContent = `[Unit]
61
- Description=Palmier Agent
62
- After=network-online.target
63
- Wants=network-online.target
64
-
65
- [Service]
66
- Type=simple
67
- ExecStart=${palmierBin} serve
68
- WorkingDirectory=${config.projectRoot}
69
- Restart=on-failure
70
- RestartSec=5
71
- Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
72
-
73
- [Install]
74
- WantedBy=default.target
150
+ const serviceContent = `[Unit]
151
+ Description=Palmier Host
152
+ After=network-online.target
153
+ Wants=network-online.target
154
+
155
+ [Service]
156
+ Type=simple
157
+ ExecStart=${palmierBin} serve
158
+ WorkingDirectory=${config.projectRoot}
159
+ Restart=on-failure
160
+ RestartSec=5
161
+ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
162
+
163
+ [Install]
164
+ WantedBy=default.target
75
165
  `;
76
- const servicePath = path.join(unitDir, "palmier-agent.service");
166
+ const servicePath = path.join(unitDir, "palmier.service");
77
167
  fs.writeFileSync(servicePath, serviceContent, "utf-8");
78
168
  console.log("Systemd service installed at:", servicePath);
79
- // 6. Enable and start the service
169
+ // Enable and start the service
80
170
  try {
81
171
  execSync("systemctl --user daemon-reload", { stdio: "inherit" });
82
- execSync("systemctl --user enable --now palmier-agent.service", { stdio: "inherit" });
83
- console.log("Palmier agent service enabled and started.");
172
+ execSync("systemctl --user enable --now palmier.service", { stdio: "inherit" });
173
+ console.log("Palmier host service enabled and started.");
84
174
  }
85
175
  catch (err) {
86
176
  console.error(`Warning: failed to enable systemd service: ${err}`);
87
- console.error("You may need to start it manually: systemctl --user enable --now palmier-agent.service");
177
+ console.error("You may need to start it manually: systemctl --user enable --now palmier.service");
88
178
  }
89
- // 7. Enable lingering so service runs without active login session
179
+ // Enable lingering so service runs without active login session
90
180
  try {
91
181
  execSync(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
92
182
  console.log("Login lingering enabled.");
@@ -94,6 +184,6 @@ WantedBy=default.target
94
184
  catch (err) {
95
185
  console.error(`Warning: failed to enable linger: ${err}`);
96
186
  }
97
- console.log("\nAgent initialization complete!");
187
+ console.log("\nHost initialization complete!");
98
188
  }
99
189
  //# sourceMappingURL=init.js.map
@@ -0,0 +1,2 @@
1
+ export declare function mcpserverCommand(): Promise<void>;
2
+ //# sourceMappingURL=mcpserver.d.ts.map
@@ -0,0 +1,75 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { StringCodec } from "nats";
5
+ import { loadConfig } from "../config.js";
6
+ import { connectNats } from "../nats-client.js";
7
+ export async function mcpserverCommand() {
8
+ const config = loadConfig();
9
+ const mode = config.mode ?? "nats";
10
+ const useNats = mode === "nats" || mode === "auto";
11
+ let nc;
12
+ if (useNats) {
13
+ nc = await connectNats(config);
14
+ }
15
+ const sc = StringCodec();
16
+ const server = new McpServer({ name: "palmier", version: "1.0.0" }, { capabilities: { tools: {} } });
17
+ // send-email requires NATS — only register in nats/auto mode
18
+ if (nc) {
19
+ server.registerTool("send-email", {
20
+ description: "Send an email via the Palmier platform's SMTP relay",
21
+ inputSchema: {
22
+ to: z.string().describe("Recipient email address(es), comma-separated"),
23
+ subject: z.string().describe("Email subject line"),
24
+ text: z.string().describe("Plain text body"),
25
+ html: z.string().optional().describe("HTML body"),
26
+ reply_to: z.string().optional().describe("Reply-To address"),
27
+ cc: z.string().optional().describe("CC recipients, comma-separated"),
28
+ bcc: z.string().optional().describe("BCC recipients, comma-separated"),
29
+ },
30
+ }, async (args) => {
31
+ const payload = {
32
+ hostId: config.hostId,
33
+ to: args.to,
34
+ subject: args.subject,
35
+ text: args.text,
36
+ html: args.html,
37
+ replyTo: args.reply_to,
38
+ cc: args.cc,
39
+ bcc: args.bcc,
40
+ };
41
+ try {
42
+ const subject = `host.${config.hostId}.email.send`;
43
+ const reply = await nc.request(subject, sc.encode(JSON.stringify(payload)), {
44
+ timeout: 15_000,
45
+ });
46
+ const result = JSON.parse(sc.decode(reply.data));
47
+ if (result.ok) {
48
+ return {
49
+ content: [{ type: "text", text: `Email sent successfully (messageId: ${result.messageId})` }],
50
+ };
51
+ }
52
+ else {
53
+ return {
54
+ content: [{ type: "text", text: `Failed to send email: ${result.error}` }],
55
+ isError: true,
56
+ };
57
+ }
58
+ }
59
+ catch (err) {
60
+ return {
61
+ content: [{ type: "text", text: `Error sending email: ${err}` }],
62
+ isError: true,
63
+ };
64
+ }
65
+ });
66
+ }
67
+ const transport = new StdioServerTransport();
68
+ await server.connect(transport);
69
+ // Graceful shutdown
70
+ transport.onclose = async () => {
71
+ if (nc)
72
+ await nc.drain();
73
+ };
74
+ }
75
+ //# sourceMappingURL=mcpserver.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Generate an OTP code and wait for a PWA client to pair.
3
+ * Listens on NATS (nats/auto modes) and/or HTTP (lan/auto modes).
4
+ */
5
+ export declare function pairCommand(): Promise<void>;
6
+ //# sourceMappingURL=pair.d.ts.map
@@ -0,0 +1,140 @@
1
+ import * as http from "node:http";
2
+ import { StringCodec } from "nats";
3
+ import { loadConfig } from "../config.js";
4
+ import { connectNats } from "../nats-client.js";
5
+ import { addSession } from "../session-store.js";
6
+ const CODE_CHARS = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"; // no O/0/I/1/L
7
+ const CODE_LENGTH = 6;
8
+ const EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
9
+ function generateCode() {
10
+ const bytes = new Uint8Array(CODE_LENGTH);
11
+ crypto.getRandomValues(bytes);
12
+ return Array.from(bytes, (b) => CODE_CHARS[b % CODE_CHARS.length]).join("");
13
+ }
14
+ function buildPairResponse(config, label) {
15
+ const session = addSession(label);
16
+ const response = {
17
+ hostId: config.hostId,
18
+ sessionToken: session.token,
19
+ };
20
+ return response;
21
+ }
22
+ /**
23
+ * POST to the running serve process and long-poll until paired or expired.
24
+ * Returns true if paired, false if expired/failed.
25
+ */
26
+ function lanPairRegister(port, code) {
27
+ const body = JSON.stringify({ code, expiryMs: EXPIRY_MS });
28
+ return new Promise((resolve) => {
29
+ const req = http.request({
30
+ hostname: "127.0.0.1",
31
+ port,
32
+ path: "/internal/pair-register",
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/json" },
35
+ timeout: EXPIRY_MS + 5000, // slightly longer than expiry
36
+ }, (res) => {
37
+ const chunks = [];
38
+ res.on("data", (chunk) => chunks.push(chunk));
39
+ res.on("end", () => {
40
+ try {
41
+ const result = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
42
+ resolve(result.paired);
43
+ }
44
+ catch {
45
+ resolve(false);
46
+ }
47
+ });
48
+ });
49
+ req.on("error", (err) => {
50
+ console.error(`Failed to reach palmier serve on port ${port}: ${err.message}`);
51
+ console.error("Make sure `palmier serve` is running first.");
52
+ resolve(false);
53
+ });
54
+ req.on("timeout", () => {
55
+ req.destroy();
56
+ resolve(false);
57
+ });
58
+ req.end(body);
59
+ });
60
+ }
61
+ /**
62
+ * Generate an OTP code and wait for a PWA client to pair.
63
+ * Listens on NATS (nats/auto modes) and/or HTTP (lan/auto modes).
64
+ */
65
+ export async function pairCommand() {
66
+ const config = loadConfig();
67
+ const code = generateCode();
68
+ const mode = config.mode ?? "nats";
69
+ let paired = false;
70
+ function onPaired() {
71
+ paired = true;
72
+ console.log("Paired successfully!");
73
+ }
74
+ const cleanups = [];
75
+ // Display pairing info
76
+ console.log("");
77
+ console.log("Enter this code in your Palmier app:");
78
+ console.log("");
79
+ console.log(` ${code}`);
80
+ console.log("");
81
+ console.log("Code expires in 5 minutes.");
82
+ // NATS pairing (nats or auto mode)
83
+ if (mode === "nats" || mode === "auto") {
84
+ const nc = await connectNats(config);
85
+ const sc = StringCodec();
86
+ const subject = `pair.${code}`;
87
+ const sub = nc.subscribe(subject, { max: 1 });
88
+ cleanups.push(async () => {
89
+ sub.unsubscribe();
90
+ await nc.drain();
91
+ });
92
+ (async () => {
93
+ for await (const msg of sub) {
94
+ if (paired)
95
+ break;
96
+ let label;
97
+ try {
98
+ if (msg.data && msg.data.length > 0) {
99
+ const body = JSON.parse(sc.decode(msg.data));
100
+ label = body.label;
101
+ }
102
+ }
103
+ catch { /* empty body is fine */ }
104
+ const response = buildPairResponse(config, label);
105
+ if (msg.reply) {
106
+ msg.respond(sc.encode(JSON.stringify(response)));
107
+ }
108
+ onPaired();
109
+ }
110
+ })();
111
+ }
112
+ // LAN pairing — long-poll the running serve process
113
+ if (mode === "lan" || mode === "auto") {
114
+ const port = config.directPort ?? 7400;
115
+ (async () => {
116
+ const result = await lanPairRegister(port, code);
117
+ if (result)
118
+ onPaired();
119
+ })();
120
+ }
121
+ // Wait for pairing or timeout
122
+ const start = Date.now();
123
+ await new Promise((resolve) => {
124
+ const interval = setInterval(() => {
125
+ if (paired || Date.now() - start >= EXPIRY_MS) {
126
+ clearInterval(interval);
127
+ resolve();
128
+ }
129
+ }, 500);
130
+ });
131
+ // Cleanup
132
+ for (const cleanup of cleanups) {
133
+ await cleanup();
134
+ }
135
+ if (!paired) {
136
+ console.log("Code expired. Run `palmier pair` to try again.");
137
+ process.exit(1);
138
+ }
139
+ }
140
+ //# sourceMappingURL=pair.js.map
@@ -0,0 +1,32 @@
1
+ You are a task planning assistant. Given a task description, produce a detailed Markdown execution plan that an agent can follow step by step. **Do not execute any part of the plan yourself.**
2
+
3
+ Your output must begin with a raw YAML frontmatter block (delimited by `---`), followed by the plan body in Markdown. Do NOT wrap the frontmatter in code fences. The very first line of your output must be `---`.
4
+
5
+ **Output format:**
6
+
7
+ ---
8
+ task_name: <short descriptive name, 3-6 words>
9
+ ---
10
+
11
+ <plan body in Markdown>
12
+
13
+ **Frontmatter fields:**
14
+
15
+ - `task_name`: A concise label for the task (e.g., "Clean up temp files", "Update system packages", "Backup database daily").
16
+
17
+ **Plan body sections:**
18
+
19
+ ### 1. Goal
20
+ State what the task accomplishes and the expected end state.
21
+
22
+ ### 2. Plan
23
+ A numbered sequence of concrete, actionable steps. Use sub-steps for complex actions and include conditional branches where behavior may vary (e.g., "If file exists, do A; otherwise, do B"). Each step should be specific enough for the agent to execute without ambiguity.
24
+
25
+ ### 3. Output Format (if applicable)
26
+ If the task produces a report, email, or other formatted output, specify the exact structure, sections, tone, and any templates the agent should follow.
27
+
28
+ Use Markdown formatting throughout — headings, code blocks, and tables where appropriate.
29
+
30
+ **Important:** Any relative times or dates in the task description (e.g., "yesterday", "last week") are relative to when the task is executed, not when this plan is generated. The agent must resolve them at execution time.
31
+
32
+ **Task description:**
@@ -1,4 +1,3 @@
1
- export type TaskEventType = "start" | "finish" | "abort" | "fail";
2
1
  /**
3
2
  * Execute a task by ID.
4
3
  */