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
package/dist/task.js CHANGED
@@ -26,7 +26,8 @@ function parseTaskContent(content) {
26
26
  if (!frontmatter.id) {
27
27
  throw new Error("TASK.md frontmatter must include at least: id");
28
28
  }
29
- frontmatter.command_line ??= "claude -p --dangerously-skip-permissions";
29
+ frontmatter.name ??= frontmatter.user_prompt?.slice(0, 60) ?? "";
30
+ frontmatter.agent ??= "claude";
30
31
  frontmatter.triggers_enabled ??= true;
31
32
  return { frontmatter, body };
32
33
  }
@@ -42,30 +43,65 @@ export function writeTaskFile(taskDir, task) {
42
43
  fs.writeFileSync(filePath, content, "utf-8");
43
44
  }
44
45
  /**
45
- * List all tasks from projectRoot/tasks/{id}/TASK.md.
46
+ * Append a task ID to the project-level tasks.jsonl file.
47
+ */
48
+ export function appendTaskList(projectRoot, taskId) {
49
+ const listPath = path.join(projectRoot, "tasks.jsonl");
50
+ fs.appendFileSync(listPath, JSON.stringify({ task_id: taskId }) + "\n", "utf-8");
51
+ }
52
+ /**
53
+ * Remove a task ID from the project-level tasks.jsonl file.
54
+ * Returns true if the entry was found and removed.
55
+ */
56
+ export function removeFromTaskList(projectRoot, taskId) {
57
+ const listPath = path.join(projectRoot, "tasks.jsonl");
58
+ if (!fs.existsSync(listPath))
59
+ return false;
60
+ const lines = fs.readFileSync(listPath, "utf-8").split("\n").filter(Boolean);
61
+ let found = false;
62
+ const remaining = [];
63
+ for (const line of lines) {
64
+ try {
65
+ const entry = JSON.parse(line);
66
+ if (entry.task_id === taskId) {
67
+ found = true;
68
+ continue;
69
+ }
70
+ }
71
+ catch { /* keep malformed lines */ }
72
+ remaining.push(line);
73
+ }
74
+ if (!found)
75
+ return false;
76
+ fs.writeFileSync(listPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
77
+ return true;
78
+ }
79
+ /**
80
+ * List all tasks referenced in tasks.jsonl.
46
81
  */
47
82
  export function listTasks(projectRoot) {
48
- const tasksDir = path.join(projectRoot, "tasks");
49
- if (!fs.existsSync(tasksDir)) {
83
+ const listPath = path.join(projectRoot, "tasks.jsonl");
84
+ if (!fs.existsSync(listPath))
50
85
  return [];
51
- }
52
- const entries = fs.readdirSync(tasksDir, { withFileTypes: true });
86
+ const lines = fs.readFileSync(listPath, "utf-8").split("\n").filter(Boolean);
53
87
  const tasks = [];
54
- for (const entry of entries) {
55
- if (!entry.isDirectory())
56
- continue;
57
- const taskDir = path.join(tasksDir, entry.name);
58
- const taskFile = path.join(taskDir, "TASK.md");
59
- if (!fs.existsSync(taskFile))
88
+ for (const line of lines) {
89
+ let taskId;
90
+ try {
91
+ taskId = JSON.parse(line).task_id;
92
+ }
93
+ catch {
60
94
  continue;
95
+ }
96
+ const taskDir = getTaskDir(projectRoot, taskId);
61
97
  try {
62
98
  tasks.push(parseTaskFile(taskDir));
63
99
  }
64
100
  catch (err) {
65
- console.error(`Warning: failed to parse task in ${taskDir}: ${err}`);
101
+ console.error(`Warning: failed to parse task ${taskId}: ${err}`);
66
102
  }
67
103
  }
68
- return tasks;
104
+ return tasks.reverse();
69
105
  }
70
106
  /**
71
107
  * Get the directory path for a task by its ID.
@@ -73,4 +109,109 @@ export function listTasks(projectRoot) {
73
109
  export function getTaskDir(projectRoot, taskId) {
74
110
  return path.join(projectRoot, "tasks", taskId);
75
111
  }
112
+ /**
113
+ * Get the creation time (birthtime) of a TASK.md file in ms since epoch.
114
+ */
115
+ export function getTaskCreatedAt(taskDir) {
116
+ const filePath = path.join(taskDir, "TASK.md");
117
+ try {
118
+ return fs.statSync(filePath).birthtimeMs;
119
+ }
120
+ catch {
121
+ return 0;
122
+ }
123
+ }
124
+ /**
125
+ * Write task status to status.json in the task directory.
126
+ */
127
+ export function writeTaskStatus(taskDir, status) {
128
+ const filePath = path.join(taskDir, "status.json");
129
+ fs.writeFileSync(filePath, JSON.stringify(status), "utf-8");
130
+ }
131
+ /**
132
+ * Read task status from status.json in the task directory.
133
+ * Returns undefined if the file doesn't exist.
134
+ */
135
+ export function readTaskStatus(taskDir) {
136
+ const filePath = path.join(taskDir, "status.json");
137
+ try {
138
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
139
+ }
140
+ catch {
141
+ return undefined;
142
+ }
143
+ }
144
+ /**
145
+ * Append a history entry to the project-level history.jsonl file.
146
+ */
147
+ export function appendHistory(projectRoot, entry) {
148
+ const historyPath = path.join(projectRoot, "history.jsonl");
149
+ fs.appendFileSync(historyPath, JSON.stringify(entry) + "\n", "utf-8");
150
+ }
151
+ /**
152
+ * Delete a history entry and its associated result/task-snapshot files.
153
+ * Returns true if the entry was found and removed.
154
+ */
155
+ export function deleteHistoryEntry(projectRoot, taskId, resultFile) {
156
+ const historyPath = path.join(projectRoot, "history.jsonl");
157
+ if (!fs.existsSync(historyPath))
158
+ return false;
159
+ const lines = fs.readFileSync(historyPath, "utf-8").split("\n").filter(Boolean);
160
+ let found = false;
161
+ const remaining = [];
162
+ for (const line of lines) {
163
+ try {
164
+ const entry = JSON.parse(line);
165
+ if (entry.task_id === taskId && entry.result_file === resultFile) {
166
+ found = true;
167
+ continue; // skip this entry
168
+ }
169
+ }
170
+ catch { /* keep malformed lines */ }
171
+ remaining.push(line);
172
+ }
173
+ if (!found)
174
+ return false;
175
+ // Rewrite history.jsonl without the deleted entry
176
+ fs.writeFileSync(historyPath, remaining.length > 0 ? remaining.join("\n") + "\n" : "", "utf-8");
177
+ // Delete the result file
178
+ const resultPath = path.join(projectRoot, "tasks", taskId, resultFile);
179
+ if (fs.existsSync(resultPath)) {
180
+ fs.unlinkSync(resultPath);
181
+ }
182
+ // Delete the corresponding task snapshot (TASK-<timestamp>.md)
183
+ const tsMatch = resultFile.match(/^RESULT-(\d+)\.md$/);
184
+ if (tsMatch) {
185
+ const snapshotFile = `TASK-${tsMatch[1]}.md`;
186
+ const snapshotPath = path.join(projectRoot, "tasks", taskId, snapshotFile);
187
+ if (fs.existsSync(snapshotPath)) {
188
+ fs.unlinkSync(snapshotPath);
189
+ }
190
+ }
191
+ return true;
192
+ }
193
+ /**
194
+ * Read history entries from history.jsonl with pagination.
195
+ * Returns entries sorted most-recent-first.
196
+ */
197
+ export function readHistory(projectRoot, opts) {
198
+ const historyPath = path.join(projectRoot, "history.jsonl");
199
+ if (!fs.existsSync(historyPath))
200
+ return { entries: [], total: 0 };
201
+ const lines = fs.readFileSync(historyPath, "utf-8").split("\n").filter(Boolean);
202
+ let all = [];
203
+ for (const line of lines) {
204
+ try {
205
+ all.push(JSON.parse(line));
206
+ }
207
+ catch { /* skip malformed */ }
208
+ }
209
+ all.reverse();
210
+ if (opts.task_id) {
211
+ all = all.filter((e) => e.task_id === opts.task_id);
212
+ }
213
+ const offset = opts.offset ?? 0;
214
+ const limit = opts.limit ?? 10;
215
+ return { entries: all.slice(offset, offset + limit), total: all.length };
216
+ }
76
217
  //# sourceMappingURL=task.js.map
@@ -0,0 +1,6 @@
1
+ import type { HostConfig, RpcMessage } from "../types.js";
2
+ /**
3
+ * Start the HTTP transport: Express-like server with RPC, SSE, and health endpoints.
4
+ */
5
+ export declare function startHttpTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>): Promise<void>;
6
+ //# sourceMappingURL=http-transport.d.ts.map
@@ -0,0 +1,243 @@
1
+ import * as http from "node:http";
2
+ import * as os from "os";
3
+ import { validateSession, hasSessions, addSession } from "../session-store.js";
4
+ const pendingPairs = new Map();
5
+ function detectLanIp() {
6
+ const interfaces = os.networkInterfaces();
7
+ for (const name of Object.keys(interfaces)) {
8
+ for (const iface of interfaces[name] ?? []) {
9
+ if (iface.family === "IPv4" && !iface.internal) {
10
+ return iface.address;
11
+ }
12
+ }
13
+ }
14
+ return "127.0.0.1";
15
+ }
16
+ /**
17
+ * Start the HTTP transport: Express-like server with RPC, SSE, and health endpoints.
18
+ */
19
+ export async function startHttpTransport(config, handleRpc) {
20
+ const port = config.directPort ?? 7400;
21
+ const sseClients = new Set();
22
+ function broadcastSseEvent(data) {
23
+ const payload = `data: ${JSON.stringify(data)}\n\n`;
24
+ for (const client of sseClients) {
25
+ client.write(payload);
26
+ }
27
+ }
28
+ function setCorsHeaders(res) {
29
+ res.setHeader("Access-Control-Allow-Origin", "*");
30
+ res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
31
+ res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
32
+ }
33
+ function checkAuth(req) {
34
+ const auth = req.headers.authorization;
35
+ if (!auth || !auth.startsWith("Bearer "))
36
+ return false;
37
+ const token = auth.slice(7);
38
+ // Accept the original directToken or any valid session token
39
+ if (token === config.directToken)
40
+ return true;
41
+ if (hasSessions() && validateSession(token))
42
+ return true;
43
+ return false;
44
+ }
45
+ function extractSessionToken(req) {
46
+ const auth = req.headers.authorization;
47
+ if (!auth || !auth.startsWith("Bearer "))
48
+ return undefined;
49
+ return auth.slice(7);
50
+ }
51
+ function sendJson(res, status, body) {
52
+ res.writeHead(status, { "Content-Type": "application/json" });
53
+ res.end(JSON.stringify(body));
54
+ }
55
+ function readBody(req) {
56
+ return new Promise((resolve, reject) => {
57
+ const chunks = [];
58
+ req.on("data", (chunk) => chunks.push(chunk));
59
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
60
+ req.on("error", reject);
61
+ });
62
+ }
63
+ function isLocalhost(req) {
64
+ const addr = req.socket.remoteAddress;
65
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
66
+ }
67
+ const server = http.createServer(async (req, res) => {
68
+ setCorsHeaders(res);
69
+ // Handle CORS preflight
70
+ if (req.method === "OPTIONS") {
71
+ res.writeHead(204);
72
+ res.end();
73
+ return;
74
+ }
75
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
76
+ const pathname = url.pathname;
77
+ // Internal event endpoint — localhost only, no auth
78
+ if (req.method === "POST" && pathname === "/internal/event") {
79
+ if (!isLocalhost(req)) {
80
+ sendJson(res, 403, { error: "localhost only" });
81
+ return;
82
+ }
83
+ try {
84
+ const body = await readBody(req);
85
+ const event = JSON.parse(body);
86
+ broadcastSseEvent(event);
87
+ sendJson(res, 200, { ok: true });
88
+ }
89
+ catch {
90
+ sendJson(res, 400, { error: "Invalid JSON" });
91
+ }
92
+ return;
93
+ }
94
+ // Internal pair-register endpoint — localhost only, long-poll
95
+ // The pair CLI posts here and blocks until paired or expired.
96
+ if (req.method === "POST" && pathname === "/internal/pair-register") {
97
+ if (!isLocalhost(req)) {
98
+ sendJson(res, 403, { error: "localhost only" });
99
+ return;
100
+ }
101
+ try {
102
+ const body = await readBody(req);
103
+ const { code, expiryMs } = JSON.parse(body);
104
+ if (!code) {
105
+ sendJson(res, 400, { error: "Missing code" });
106
+ return;
107
+ }
108
+ if (pendingPairs.has(code)) {
109
+ sendJson(res, 409, { error: "Code already registered" });
110
+ return;
111
+ }
112
+ const result = await new Promise((resolve) => {
113
+ const timer = setTimeout(() => {
114
+ pendingPairs.delete(code);
115
+ resolve({ paired: false });
116
+ }, expiryMs ?? 5 * 60 * 1000);
117
+ pendingPairs.set(code, { resolve, timer });
118
+ // Clean up if the CLI disconnects early
119
+ req.on("close", () => {
120
+ if (pendingPairs.has(code)) {
121
+ clearTimeout(timer);
122
+ pendingPairs.delete(code);
123
+ }
124
+ });
125
+ });
126
+ sendJson(res, 200, result);
127
+ }
128
+ catch {
129
+ sendJson(res, 400, { error: "Invalid JSON" });
130
+ }
131
+ return;
132
+ }
133
+ // Public pair endpoint — no auth required, PWA posts OTP code here
134
+ if (req.method === "POST" && pathname === "/pair") {
135
+ try {
136
+ const body = await readBody(req);
137
+ const { code, label } = JSON.parse(body);
138
+ if (!code) {
139
+ sendJson(res, 400, { error: "Missing code" });
140
+ return;
141
+ }
142
+ const pending = pendingPairs.get(code);
143
+ if (!pending) {
144
+ sendJson(res, 401, { error: "Invalid code" });
145
+ return;
146
+ }
147
+ // Create session and build response
148
+ const session = addSession(label);
149
+ const ip = detectLanIp();
150
+ const response = {
151
+ hostId: config.hostId,
152
+ sessionToken: session.token,
153
+ directUrl: `http://${ip}:${port}`,
154
+ directToken: config.directToken,
155
+ };
156
+ // Resolve the long-poll and clean up
157
+ clearTimeout(pending.timer);
158
+ pendingPairs.delete(code);
159
+ pending.resolve({ paired: true });
160
+ sendJson(res, 200, response);
161
+ }
162
+ catch {
163
+ sendJson(res, 400, { error: "Invalid JSON" });
164
+ }
165
+ return;
166
+ }
167
+ // All other endpoints require auth
168
+ if (!checkAuth(req)) {
169
+ sendJson(res, 401, { error: "Unauthorized" });
170
+ return;
171
+ }
172
+ // SSE event stream
173
+ if (req.method === "GET" && pathname === "/events") {
174
+ res.writeHead(200, {
175
+ "Content-Type": "text/event-stream",
176
+ "Cache-Control": "no-cache",
177
+ Connection: "keep-alive",
178
+ "Access-Control-Allow-Origin": "*",
179
+ });
180
+ res.write(":ok\n\n");
181
+ // Send heartbeat every 5 seconds
182
+ const heartbeat = setInterval(() => {
183
+ res.write("data: {\"heartbeat\":true}\n\n");
184
+ }, 5000);
185
+ sseClients.add(res);
186
+ req.on("close", () => {
187
+ clearInterval(heartbeat);
188
+ sseClients.delete(res);
189
+ });
190
+ return;
191
+ }
192
+ // RPC endpoint: POST /rpc/<method>
193
+ if (req.method === "POST" && pathname.startsWith("/rpc/")) {
194
+ const method = pathname.slice("/rpc/".length);
195
+ if (!method) {
196
+ sendJson(res, 400, { error: "Missing RPC method" });
197
+ return;
198
+ }
199
+ let params = {};
200
+ try {
201
+ const body = await readBody(req);
202
+ if (body.trim().length > 0) {
203
+ params = JSON.parse(body);
204
+ }
205
+ }
206
+ catch {
207
+ sendJson(res, 400, { error: "Invalid JSON" });
208
+ return;
209
+ }
210
+ const sessionToken = extractSessionToken(req);
211
+ console.log(`[http] RPC: ${method}`);
212
+ try {
213
+ const response = await handleRpc({ method, params, sessionToken });
214
+ console.log(`[http] RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
215
+ sendJson(res, 200, response);
216
+ }
217
+ catch (err) {
218
+ console.error(`[http] RPC error (${method}):`, err);
219
+ sendJson(res, 500, { error: String(err) });
220
+ }
221
+ return;
222
+ }
223
+ sendJson(res, 404, { error: "Not found" });
224
+ });
225
+ return new Promise((resolve, reject) => {
226
+ server.listen(port, () => {
227
+ console.log(`[http] Listening on port ${port}`);
228
+ console.log(`[http] SSE clients can connect to /events`);
229
+ // Graceful shutdown
230
+ const shutdown = () => {
231
+ console.log("[http] Shutting down...");
232
+ for (const client of sseClients) {
233
+ client.end();
234
+ }
235
+ server.close(() => process.exit(0));
236
+ };
237
+ process.on("SIGINT", shutdown);
238
+ process.on("SIGTERM", shutdown);
239
+ });
240
+ server.on("error", reject);
241
+ });
242
+ }
243
+ //# sourceMappingURL=http-transport.js.map
@@ -0,0 +1,6 @@
1
+ import type { HostConfig, RpcMessage } from "../types.js";
2
+ /**
3
+ * Start the NATS transport: connect, subscribe to RPC subjects, dispatch to handler.
4
+ */
5
+ export declare function startNatsTransport(config: HostConfig, handleRpc: (req: RpcMessage) => Promise<unknown>): Promise<void>;
6
+ //# sourceMappingURL=nats-transport.d.ts.map
@@ -0,0 +1,69 @@
1
+ import { StringCodec } from "nats";
2
+ import { connectNats } from "../nats-client.js";
3
+ /**
4
+ * Start the NATS transport: connect, subscribe to RPC subjects, dispatch to handler.
5
+ */
6
+ export async function startNatsTransport(config, handleRpc) {
7
+ const nc = await connectNats(config);
8
+ const sc = StringCodec();
9
+ const subject = `host.${config.hostId}.rpc.>`;
10
+ console.log(`[nats] Subscribing to: ${subject}`);
11
+ const sub = nc.subscribe(subject);
12
+ // Graceful shutdown
13
+ const shutdown = async () => {
14
+ console.log("[nats] Shutting down...");
15
+ sub.unsubscribe();
16
+ await nc.drain();
17
+ process.exit(0);
18
+ };
19
+ process.on("SIGINT", shutdown);
20
+ process.on("SIGTERM", shutdown);
21
+ async function processMessage(msg) {
22
+ // Derive RPC method from subject: ...rpc.<method parts>
23
+ const subjectTokens = msg.subject.split(".");
24
+ const rpcIdx = subjectTokens.indexOf("rpc");
25
+ const method = rpcIdx >= 0 ? subjectTokens.slice(rpcIdx + 1).join(".") : "";
26
+ // Parse params from message body
27
+ let params = {};
28
+ if (msg.data && msg.data.length > 0) {
29
+ const raw = sc.decode(msg.data).trim();
30
+ if (raw.length > 0) {
31
+ try {
32
+ params = JSON.parse(raw);
33
+ }
34
+ catch {
35
+ console.error(`[nats] Failed to parse RPC params for ${method}`);
36
+ if (msg.reply) {
37
+ msg.respond(sc.encode(JSON.stringify({ error: "Invalid JSON" })));
38
+ }
39
+ return;
40
+ }
41
+ }
42
+ }
43
+ // Extract sessionToken from params (PWA includes it in the payload)
44
+ const sessionToken = typeof params.sessionToken === "string" ? params.sessionToken : undefined;
45
+ delete params.sessionToken;
46
+ console.log(`[nats] RPC: ${method}`);
47
+ let response;
48
+ try {
49
+ response = await handleRpc({ method, params, sessionToken });
50
+ }
51
+ catch (err) {
52
+ console.error(`[nats] RPC error (${method}):`, err);
53
+ response = { error: String(err) };
54
+ }
55
+ console.log(`[nats] RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
56
+ if (msg.reply) {
57
+ msg.respond(sc.encode(JSON.stringify(response)));
58
+ }
59
+ }
60
+ async function consumeSubscription(subscription) {
61
+ for await (const msg of subscription) {
62
+ // Handle RPC without blocking the message loop so heartbeats keep flowing
63
+ processMessage(msg);
64
+ }
65
+ }
66
+ console.log("[nats] Waiting for RPC messages...");
67
+ await consumeSubscription(sub);
68
+ }
69
+ //# sourceMappingURL=nats-transport.js.map
package/dist/types.d.ts CHANGED
@@ -1,18 +1,26 @@
1
- export interface AgentConfig {
2
- agentId: string;
3
- userId: string;
4
- natsUrl: string;
5
- natsWsUrl: string;
6
- natsToken: string;
1
+ export interface HostConfig {
2
+ hostId: string;
7
3
  projectRoot: string;
4
+ mode: "nats" | "lan" | "auto";
5
+ natsUrl?: string;
6
+ natsWsUrl?: string;
7
+ natsToken?: string;
8
+ directPort?: number;
9
+ directToken?: string;
10
+ agents?: Array<{
11
+ key: string;
12
+ label: string;
13
+ }>;
8
14
  }
9
15
  export interface TaskFrontmatter {
10
16
  id: string;
17
+ name: string;
11
18
  user_prompt: string;
12
- command_line: string;
19
+ agent: string;
13
20
  triggers: Trigger[];
14
21
  triggers_enabled: boolean;
15
22
  requires_confirmation: boolean;
23
+ permissions?: RequiredPermission[];
16
24
  }
17
25
  export interface Trigger {
18
26
  type: "cron" | "once";
@@ -22,16 +30,25 @@ export interface ParsedTask {
22
30
  frontmatter: TaskFrontmatter;
23
31
  body: string;
24
32
  }
25
- export interface ConfirmPayload {
26
- type: "confirm";
33
+ export type TaskRunningState = "start" | "finish" | "abort" | "fail";
34
+ export interface TaskStatus {
35
+ running_state: TaskRunningState;
36
+ time_stamp: number;
37
+ pending_confirmation?: boolean;
38
+ pending_permission?: RequiredPermission[];
39
+ user_input?: string;
40
+ }
41
+ export interface HistoryEntry {
27
42
  task_id: string;
28
- agent_id: string;
29
- user_id: string;
30
- details: Record<string, unknown>;
31
- status: "pending" | "confirmed" | "aborted";
43
+ result_file: string;
44
+ }
45
+ export interface RequiredPermission {
46
+ name: string;
47
+ description: string;
32
48
  }
33
49
  export interface RpcMessage {
34
50
  method: string;
35
51
  params: Record<string, unknown>;
52
+ sessionToken?: string;
36
53
  }
37
54
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.2.0",
4
- "description": "Palmier agent CLI - provisions, executes tasks, and serves NATS RPC",
3
+ "version": "0.2.2",
4
+ "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "ISC",
6
6
  "author": "Hongxu Cai",
7
7
  "type": "module",
@@ -12,11 +12,12 @@
12
12
  },
13
13
  "scripts": {
14
14
  "dev": "tsx src/index.ts",
15
- "build": "tsc && node -e \"require('fs').cpSync('src/commands/task-generation.md','dist/commands/task-generation.md')\"",
15
+ "build": "tsc && node -e \"require('fs').cpSync('src/commands/plan-generation.md','dist/commands/plan-generation.md')\"",
16
16
  "prepare": "npm run build",
17
17
  "start": "node dist/index.js"
18
18
  },
19
19
  "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.27.1",
20
21
  "commander": "^13.1.0",
21
22
  "dotenv": "^16.4.7",
22
23
  "nats": "^2.29.1",
@@ -0,0 +1,62 @@
1
+ import type { ParsedTask, RequiredPermission } from "../types.js";
2
+ import { ClaudeAgent } from "./claude.js";
3
+ import { GeminiAgent } from "./gemini.js";
4
+ import { CodexAgent } from "./codex.js";
5
+
6
+ export interface CommandLine {
7
+ command: string;
8
+ args: string[];
9
+ }
10
+
11
+ /**
12
+ * Interface that each agent tool must implement.
13
+ * Abstracts how plans are generated and tasks are executed across different AI agents.
14
+ */
15
+ export interface AgentTool {
16
+ /** Return the command and args used to generate a plan from a prompt. */
17
+ getPlanGenerationCommandLine(prompt: string): CommandLine;
18
+
19
+ /** Return the command and args used to run a task. If prompt is provided, use it instead of the task's prompt.
20
+ * extraPermissions are transient permissions granted for this run only (not persisted in frontmatter). */
21
+ getTaskRunCommandLine(task: ParsedTask, prompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
22
+
23
+ /** Detect whether the agent CLI is available and perform any agent-specific
24
+ * initialization. Returns true if the agent was detected and initialized successfully. */
25
+ init(): Promise<boolean>;
26
+ }
27
+
28
+ const agentRegistry: Record<string, AgentTool> = {
29
+ claude: new ClaudeAgent(),
30
+ gemini: new GeminiAgent(),
31
+ codex: new CodexAgent(),
32
+ };
33
+
34
+ const agentLabels: Record<string, string> = {
35
+ claude: "Claude Code",
36
+ gemini: "Gemini CLI",
37
+ codex: "Codex CLI",
38
+ openclaw: "OpenClaw"
39
+ };
40
+
41
+ export interface DetectedAgent {
42
+ key: string;
43
+ label: string;
44
+ }
45
+
46
+ export async function detectAgents(): Promise<DetectedAgent[]> {
47
+ const detected: DetectedAgent[] = [];
48
+ for (const [key, agent] of Object.entries(agentRegistry)) {
49
+ const label = agentLabels[key] ?? key;
50
+ const ok = await agent.init();
51
+ if (ok) detected.push({ key, label });
52
+ }
53
+ return detected;
54
+ }
55
+
56
+ export function getAgent(name: string): AgentTool {
57
+ const agent = agentRegistry[name];
58
+ if (!agent) {
59
+ throw new Error(`Unknown agent: "${name}". Available agents: ${Object.keys(agentRegistry).join(", ")}`);
60
+ }
61
+ return agent;
62
+ }