palmier 0.9.6 → 0.9.8

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 (255) hide show
  1. package/README.md +28 -13
  2. package/dist/agents/agent.d.ts +0 -1
  3. package/dist/agents/agent.js +0 -1
  4. package/dist/agents/aider.d.ts +0 -1
  5. package/dist/agents/aider.js +0 -1
  6. package/dist/agents/claude.d.ts +0 -1
  7. package/dist/agents/claude.js +0 -1
  8. package/dist/agents/cline.d.ts +0 -1
  9. package/dist/agents/cline.js +0 -1
  10. package/dist/agents/codex.d.ts +0 -1
  11. package/dist/agents/codex.js +0 -1
  12. package/dist/agents/copilot.d.ts +0 -1
  13. package/dist/agents/copilot.js +0 -1
  14. package/dist/agents/cursor.d.ts +0 -1
  15. package/dist/agents/cursor.js +0 -1
  16. package/dist/agents/deepagents.d.ts +0 -1
  17. package/dist/agents/deepagents.js +0 -1
  18. package/dist/agents/droid.d.ts +0 -1
  19. package/dist/agents/droid.js +0 -1
  20. package/dist/agents/gemini.d.ts +0 -1
  21. package/dist/agents/gemini.js +0 -1
  22. package/dist/agents/goose.d.ts +0 -1
  23. package/dist/agents/goose.js +0 -1
  24. package/dist/agents/hermes.d.ts +0 -1
  25. package/dist/agents/hermes.js +0 -1
  26. package/dist/agents/kimi.d.ts +0 -1
  27. package/dist/agents/kimi.js +0 -1
  28. package/dist/agents/kiro.d.ts +0 -1
  29. package/dist/agents/kiro.js +0 -1
  30. package/dist/agents/openclaw.d.ts +0 -1
  31. package/dist/agents/openclaw.js +0 -1
  32. package/dist/agents/opencode.d.ts +0 -1
  33. package/dist/agents/opencode.js +0 -1
  34. package/dist/agents/qoder.d.ts +0 -1
  35. package/dist/agents/qoder.js +0 -1
  36. package/dist/agents/qwen.d.ts +0 -1
  37. package/dist/agents/qwen.js +0 -1
  38. package/dist/agents/shared-prompt.d.ts +0 -1
  39. package/dist/agents/shared-prompt.js +0 -1
  40. package/dist/client-store.d.ts +0 -1
  41. package/dist/client-store.js +0 -1
  42. package/dist/commands/clients.d.ts +0 -1
  43. package/dist/commands/clients.js +0 -1
  44. package/dist/commands/info.d.ts +0 -1
  45. package/dist/commands/info.js +0 -1
  46. package/dist/commands/init.d.ts +0 -1
  47. package/dist/commands/init.js +1 -2
  48. package/dist/commands/pair.d.ts +0 -1
  49. package/dist/commands/pair.js +0 -1
  50. package/dist/commands/restart.d.ts +0 -1
  51. package/dist/commands/restart.js +0 -1
  52. package/dist/commands/run.d.ts +0 -1
  53. package/dist/commands/run.js +19 -3
  54. package/dist/commands/serve.d.ts +0 -1
  55. package/dist/commands/serve.js +0 -1
  56. package/dist/commands/uninstall.d.ts +0 -1
  57. package/dist/commands/uninstall.js +0 -1
  58. package/dist/config.d.ts +0 -1
  59. package/dist/config.js +0 -1
  60. package/dist/event-queues.d.ts +0 -1
  61. package/dist/event-queues.js +0 -1
  62. package/dist/events.d.ts +0 -1
  63. package/dist/events.js +0 -1
  64. package/dist/index.d.ts +0 -1
  65. package/dist/index.js +0 -1
  66. package/dist/linked-device.d.ts +0 -1
  67. package/dist/linked-device.js +0 -1
  68. package/dist/mcp-handler.d.ts +0 -1
  69. package/dist/mcp-handler.js +0 -1
  70. package/dist/mcp-tools.d.ts +0 -1
  71. package/dist/mcp-tools.js +0 -1
  72. package/dist/nats-client.d.ts +0 -1
  73. package/dist/nats-client.js +0 -1
  74. package/dist/network.d.ts +0 -1
  75. package/dist/network.js +0 -1
  76. package/dist/notification-store.d.ts +0 -1
  77. package/dist/notification-store.js +0 -1
  78. package/dist/pending-requests.d.ts +0 -1
  79. package/dist/pending-requests.js +0 -1
  80. package/dist/platform/index.d.ts +0 -1
  81. package/dist/platform/index.js +0 -1
  82. package/dist/platform/linux.d.ts +0 -1
  83. package/dist/platform/linux.js +0 -1
  84. package/dist/platform/macos.d.ts +0 -1
  85. package/dist/platform/macos.js +0 -1
  86. package/dist/platform/platform.d.ts +0 -1
  87. package/dist/platform/platform.js +0 -1
  88. package/dist/platform/windows.d.ts +0 -1
  89. package/dist/platform/windows.js +0 -1
  90. package/dist/pwa/assets/{index-MLEFUP3r.js → index-DWvRAUiy.js} +31 -31
  91. package/dist/pwa/assets/{web-B1sKCc7e.js → web-C4iZbqTC.js} +1 -1
  92. package/dist/pwa/assets/{web-ETD-8ZHd.js → web-CBFqJGX6.js} +1 -1
  93. package/dist/pwa/assets/{web-B4xEa6WO.js → web-DL4uXOpS.js} +1 -1
  94. package/dist/pwa/index.html +2 -2
  95. package/dist/rpc-handler.d.ts +0 -1
  96. package/dist/rpc-handler.js +0 -1
  97. package/dist/sms-store.d.ts +0 -1
  98. package/dist/sms-store.js +0 -1
  99. package/dist/spawn-command.d.ts +0 -1
  100. package/dist/spawn-command.js +0 -1
  101. package/dist/task.d.ts +0 -1
  102. package/dist/task.js +0 -1
  103. package/dist/transports/http-transport.d.ts +0 -1
  104. package/dist/transports/http-transport.js +0 -1
  105. package/dist/transports/nats-transport.d.ts +0 -1
  106. package/dist/transports/nats-transport.js +0 -1
  107. package/dist/types.d.ts +0 -1
  108. package/dist/types.js +0 -1
  109. package/dist/update-checker.d.ts +0 -1
  110. package/dist/update-checker.js +0 -1
  111. package/package.json +11 -1
  112. package/.github/workflows/ci.yml +0 -16
  113. package/.github/workflows/publish.yml +0 -37
  114. package/CLAUDE.md +0 -22
  115. package/dist/pwa/apple-touch-icon.png +0 -0
  116. package/dist/pwa/manifest.webmanifest +0 -1
  117. package/dist/pwa/pwa-192x192.png +0 -0
  118. package/dist/pwa/pwa-512x512.png +0 -0
  119. package/dist/pwa/registerSW.js +0 -1
  120. package/dist/pwa/service-worker.js +0 -2
  121. package/palmier-server/.github/workflows/ci.yml +0 -21
  122. package/palmier-server/.github/workflows/deploy.yml +0 -38
  123. package/palmier-server/CLAUDE.md +0 -17
  124. package/palmier-server/PRODUCTION.md +0 -358
  125. package/palmier-server/README.md +0 -231
  126. package/palmier-server/nats.conf +0 -19
  127. package/palmier-server/package.json +0 -15
  128. package/palmier-server/pnpm-lock.yaml +0 -7639
  129. package/palmier-server/pnpm-workspace.yaml +0 -3
  130. package/palmier-server/pwa/index.html +0 -16
  131. package/palmier-server/pwa/logo/logo_20260421.png +0 -0
  132. package/palmier-server/pwa/package.json +0 -34
  133. package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
  134. package/palmier-server/pwa/public/favicon.ico +0 -0
  135. package/palmier-server/pwa/public/pwa-192x192.png +0 -0
  136. package/palmier-server/pwa/public/pwa-512x512.png +0 -0
  137. package/palmier-server/pwa/src/App.css +0 -3012
  138. package/palmier-server/pwa/src/App.tsx +0 -59
  139. package/palmier-server/pwa/src/agentLabels.ts +0 -11
  140. package/palmier-server/pwa/src/api.ts +0 -67
  141. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
  142. package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -113
  143. package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
  144. package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
  145. package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
  146. package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
  147. package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
  148. package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
  149. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
  150. package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
  151. package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
  152. package/palmier-server/pwa/src/components/TaskForm.tsx +0 -766
  153. package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
  154. package/palmier-server/pwa/src/constants.ts +0 -2
  155. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
  156. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
  157. package/palmier-server/pwa/src/draftGuard.ts +0 -24
  158. package/palmier-server/pwa/src/formatTime.ts +0 -44
  159. package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
  160. package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
  161. package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
  162. package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
  163. package/palmier-server/pwa/src/main.tsx +0 -14
  164. package/palmier-server/pwa/src/native/Device.ts +0 -49
  165. package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
  166. package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
  167. package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
  168. package/palmier-server/pwa/src/service-worker.ts +0 -142
  169. package/palmier-server/pwa/src/types.ts +0 -75
  170. package/palmier-server/pwa/src/vite-env.d.ts +0 -11
  171. package/palmier-server/pwa/tsconfig.json +0 -21
  172. package/palmier-server/pwa/tsconfig.node.json +0 -19
  173. package/palmier-server/pwa/vite.config.ts +0 -47
  174. package/palmier-server/server/.env.example +0 -20
  175. package/palmier-server/server/package.json +0 -36
  176. package/palmier-server/server/src/db.ts +0 -44
  177. package/palmier-server/server/src/fcm.ts +0 -74
  178. package/palmier-server/server/src/index.ts +0 -688
  179. package/palmier-server/server/src/nats-jwt.ts +0 -299
  180. package/palmier-server/server/src/nats-setup.ts +0 -48
  181. package/palmier-server/server/src/nats.ts +0 -33
  182. package/palmier-server/server/src/notify.ts +0 -34
  183. package/palmier-server/server/src/push.ts +0 -68
  184. package/palmier-server/server/src/routes/device.ts +0 -224
  185. package/palmier-server/server/src/routes/fcm.ts +0 -64
  186. package/palmier-server/server/src/routes/hosts.ts +0 -56
  187. package/palmier-server/server/src/routes/push.ts +0 -101
  188. package/palmier-server/server/tsconfig.json +0 -20
  189. package/palmier-server/spec.md +0 -533
  190. package/src/agents/agent-instructions.md +0 -28
  191. package/src/agents/agent.ts +0 -114
  192. package/src/agents/aider.ts +0 -35
  193. package/src/agents/claude.ts +0 -39
  194. package/src/agents/cline.ts +0 -35
  195. package/src/agents/codex.ts +0 -40
  196. package/src/agents/copilot.ts +0 -37
  197. package/src/agents/cursor.ts +0 -36
  198. package/src/agents/deepagents.ts +0 -36
  199. package/src/agents/droid.ts +0 -35
  200. package/src/agents/gemini.ts +0 -43
  201. package/src/agents/goose.ts +0 -33
  202. package/src/agents/hermes.ts +0 -36
  203. package/src/agents/kimi.ts +0 -35
  204. package/src/agents/kiro.ts +0 -36
  205. package/src/agents/openclaw.ts +0 -29
  206. package/src/agents/opencode.ts +0 -36
  207. package/src/agents/qoder.ts +0 -36
  208. package/src/agents/qwen.ts +0 -32
  209. package/src/agents/shared-prompt.ts +0 -30
  210. package/src/client-store.ts +0 -68
  211. package/src/commands/clients.ts +0 -29
  212. package/src/commands/info.ts +0 -29
  213. package/src/commands/init.ts +0 -165
  214. package/src/commands/pair.ts +0 -137
  215. package/src/commands/restart.ts +0 -6
  216. package/src/commands/run.ts +0 -608
  217. package/src/commands/serve.ts +0 -211
  218. package/src/commands/uninstall.ts +0 -9
  219. package/src/config.ts +0 -36
  220. package/src/cross-spawn.d.ts +0 -5
  221. package/src/event-queues.ts +0 -41
  222. package/src/events.ts +0 -29
  223. package/src/index.ts +0 -111
  224. package/src/linked-device.ts +0 -52
  225. package/src/mcp-handler.ts +0 -200
  226. package/src/mcp-tools.ts +0 -839
  227. package/src/nats-client.ts +0 -19
  228. package/src/network.ts +0 -96
  229. package/src/notification-store.ts +0 -30
  230. package/src/pending-requests.ts +0 -73
  231. package/src/platform/index.ts +0 -20
  232. package/src/platform/linux.ts +0 -296
  233. package/src/platform/macos.ts +0 -329
  234. package/src/platform/platform.ts +0 -31
  235. package/src/platform/windows.ts +0 -299
  236. package/src/rpc-handler.ts +0 -691
  237. package/src/sms-store.ts +0 -28
  238. package/src/spawn-command.ts +0 -123
  239. package/src/task.ts +0 -343
  240. package/src/transports/http-transport.ts +0 -478
  241. package/src/transports/nats-transport.ts +0 -76
  242. package/src/types.ts +0 -89
  243. package/src/update-checker.ts +0 -40
  244. package/test/agent-instructions.test.ts +0 -209
  245. package/test/agent-output-parsing.test.ts +0 -74
  246. package/test/linux-cron.test.ts +0 -41
  247. package/test/macos-plist.test.ts +0 -112
  248. package/test/notification-store.test.ts +0 -57
  249. package/test/pairing.test.ts +0 -35
  250. package/test/result-state.test.ts +0 -110
  251. package/test/task-parsing.test.ts +0 -82
  252. package/test/taskrun-messages.test.ts +0 -224
  253. package/test/tsconfig.json +0 -9
  254. package/test/windows-xml.test.ts +0 -89
  255. package/tsconfig.json +0 -19
@@ -1,478 +0,0 @@
1
- import * as http from "node:http";
2
- import * as os from "os";
3
- import * as path from "node:path";
4
- import { StringCodec, type NatsConnection } from "nats";
5
- import { validateClient, addClient } from "../client-store.js";
6
- import { registerPending } from "../pending-requests.js";
7
- import * as fs from "node:fs";
8
- import type { HostConfig, RpcMessage, RequiredPermission } from "../types.js";
9
- import { saveConfig } from "../config.js";
10
- import { detectDefaultInterface } from "../network.js";
11
- import { agentToolMap, agentResources, ToolError, type ToolContext } from "../mcp-tools.js";
12
- import { handleMcpRequest, getAgentName, getResourceSubscriptions } from "../mcp-handler.js";
13
- import { getTaskDir } from "../task.js";
14
- import { popEvent } from "../event-queues.js";
15
-
16
- interface CachedAsset {
17
- data: Buffer;
18
- contentType: string;
19
- }
20
-
21
- const assetCache = new Map<string, CachedAsset>();
22
-
23
- const PWA_DIR = path.join(import.meta.dirname, "..", "pwa");
24
-
25
- const CONTENT_TYPES: Record<string, string> = {
26
- ".html": "text/html; charset=utf-8",
27
- ".js": "application/javascript",
28
- ".css": "text/css",
29
- ".json": "application/json",
30
- ".png": "image/png",
31
- ".ico": "image/x-icon",
32
- ".woff2": "font/woff2",
33
- ".woff": "font/woff",
34
- ".svg": "image/svg+xml",
35
- ".webmanifest": "application/manifest+json",
36
- };
37
-
38
- function guessContentType(urlPath: string): string {
39
- if (urlPath === "/") return "text/html; charset=utf-8";
40
- const ext = urlPath.match(/\.[^.]+$/)?.[0] ?? "";
41
- return CONTENT_TYPES[ext] ?? "application/octet-stream";
42
- }
43
-
44
- function getAsset(urlPath: string): CachedAsset | null {
45
- const cached = assetCache.get(urlPath);
46
- if (cached) return cached;
47
-
48
- const filePath = path.join(PWA_DIR, urlPath === "/" ? "index.html" : urlPath);
49
-
50
- // Prevent path traversal.
51
- if (!filePath.startsWith(PWA_DIR)) return null;
52
-
53
- try {
54
- let data = fs.readFileSync(filePath);
55
- // Marker lets the PWA detect it's served by palmier.
56
- if (urlPath === "/") {
57
- const html = data.toString("utf-8").replace("</head>", "<script>window.__PALMIER_SERVE__=true</script></head>");
58
- data = Buffer.from(html, "utf-8");
59
- }
60
- const asset: CachedAsset = { data, contentType: guessContentType(urlPath) };
61
- assetCache.set(urlPath, asset);
62
- return asset;
63
- } catch {
64
- return null;
65
- }
66
- }
67
-
68
- type SseClient = http.ServerResponse;
69
-
70
- interface PendingPair {
71
- resolve: (result: { paired: boolean }) => void;
72
- timer: ReturnType<typeof setTimeout>;
73
- }
74
-
75
- const pendingPairs = new Map<string, PendingPair>();
76
-
77
- async function persistDefaultInterface(config: HostConfig): Promise<void> {
78
- const iface = await detectDefaultInterface();
79
- if (iface && iface !== config.defaultInterface) {
80
- config.defaultInterface = iface;
81
- saveConfig(config);
82
- }
83
- }
84
-
85
- export async function startHttpTransport(
86
- config: HostConfig,
87
- handleRpc: (req: RpcMessage) => Promise<unknown>,
88
- port: number,
89
- nc: NatsConnection | undefined,
90
- pairingCode?: string,
91
- onReady?: () => void,
92
- ): Promise<void> {
93
- const sseClients = new Set<SseClient>();
94
- const mcpStreams = new Map<string, http.ServerResponse>();
95
- // Always bind 0.0.0.0 so other devices on the LAN can reach /rpc and /health.
96
- // The web UI, /pair, and /events are individually gated to loopback.
97
- const bindAddress = "0.0.0.0";
98
-
99
- /** Push notifications/resources/updated to all MCP clients subscribed to the given URI. */
100
- function broadcastResourceUpdated(uri: string) {
101
- const subs = getResourceSubscriptions();
102
- for (const [sessionId, uris] of subs) {
103
- if (!uris.has(uri)) continue;
104
- const stream = mcpStreams.get(sessionId);
105
- if (!stream) continue;
106
- stream.write(`data: ${JSON.stringify({
107
- jsonrpc: "2.0",
108
- method: "notifications/resources/updated",
109
- params: { uri },
110
- })}\n\n`);
111
- }
112
- }
113
-
114
- for (const resource of agentResources) {
115
- resource.subscribe(() => broadcastResourceUpdated(resource.uri));
116
- }
117
-
118
- if (pairingCode) {
119
- const EXPIRY_MS = 24 * 60 * 60 * 1000;
120
- const timer = setTimeout(() => { pendingPairs.delete(pairingCode); }, EXPIRY_MS);
121
- pendingPairs.set(pairingCode, { resolve: () => {}, timer });
122
- }
123
-
124
- function broadcastSseEvent(data: unknown) {
125
- const payload = `data: ${JSON.stringify(data)}\n\n`;
126
- for (const client of sseClients) {
127
- client.write(payload);
128
- }
129
- }
130
-
131
- function checkAuth(req: http.IncomingMessage): boolean {
132
- const auth = req.headers.authorization;
133
- if (!auth || !auth.startsWith("Bearer ")) return false;
134
- return validateClient(auth.slice(7));
135
- }
136
-
137
- function extractClientToken(req: http.IncomingMessage): string | undefined {
138
- const auth = req.headers.authorization;
139
- if (!auth || !auth.startsWith("Bearer ")) return undefined;
140
- return auth.slice(7);
141
- }
142
-
143
- function sendJson(res: http.ServerResponse, status: number, body: unknown) {
144
- res.writeHead(status, { "Content-Type": "application/json" });
145
- res.end(JSON.stringify(body));
146
- }
147
-
148
- function readBody(req: http.IncomingMessage): Promise<string> {
149
- return new Promise((resolve, reject) => {
150
- const chunks: Buffer[] = [];
151
- req.on("data", (chunk: Buffer) => chunks.push(chunk));
152
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
153
- req.on("error", reject);
154
- });
155
- }
156
-
157
- function isLocalhost(req: http.IncomingMessage): boolean {
158
- const addr = req.socket.remoteAddress;
159
- return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
160
- }
161
-
162
- async function publishEvent(taskId: string, payload: Record<string, unknown>): Promise<void> {
163
- const sc = StringCodec();
164
- const subject = `host-event.${config.hostId}.${taskId}`;
165
- if (nc) {
166
- nc.publish(subject, sc.encode(JSON.stringify(payload)));
167
- }
168
- broadcastSseEvent({ task_id: taskId, ...payload });
169
- }
170
-
171
- function makeToolContext(sessionId: string): ToolContext {
172
- return { config, nc, publishEvent, sessionId, agentName: getAgentName(sessionId) };
173
- }
174
-
175
- const server = http.createServer(async (req, res) => {
176
- const url = new URL(req.url ?? "/", `http://localhost:${port}`);
177
- const pathname = url.pathname;
178
-
179
- if (req.method === "GET" && pathname === "/health") {
180
- res.writeHead(200, {
181
- "Content-Type": "application/json",
182
- "Access-Control-Allow-Origin": "*",
183
- });
184
- res.end(JSON.stringify({ ok: true, hostId: config.hostId }));
185
- return;
186
- }
187
-
188
- if (req.method === "POST" && pathname === "/mcp") {
189
- if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
190
- try {
191
- const body = await readBody(req);
192
- const sessionId = req.headers["mcp-session-id"] as string | undefined;
193
- const ctx = makeToolContext(sessionId ?? "");
194
- const result = await handleMcpRequest(body, sessionId, ctx);
195
- if (result.sessionId) {
196
- res.setHeader("Mcp-Session-Id", result.sessionId);
197
- }
198
- if (result.stream && sessionId) {
199
- // Keep the response open as SSE for server-initiated notifications.
200
- res.writeHead(200, {
201
- "Content-Type": "text/event-stream",
202
- "Cache-Control": "no-cache",
203
- "Connection": "keep-alive",
204
- });
205
- res.write(`data: ${JSON.stringify(result.body)}\n\n`);
206
- mcpStreams.set(sessionId, res);
207
- const heartbeat = setInterval(() => { res.write(":heartbeat\n\n"); }, 15_000);
208
- req.on("close", () => {
209
- clearInterval(heartbeat);
210
- mcpStreams.delete(sessionId);
211
- getResourceSubscriptions().delete(sessionId);
212
- });
213
- } else {
214
- sendJson(res, 200, result.body);
215
- }
216
- } catch (err) {
217
- sendJson(res, 500, { error: String(err) });
218
- }
219
- return;
220
- }
221
-
222
- if (req.method === "POST" && agentToolMap.has(pathname.slice(1))) {
223
- if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
224
- const tool = agentToolMap.get(pathname.slice(1))!;
225
- try {
226
- const taskId = url.searchParams.get("taskId");
227
- if (!taskId) {
228
- sendJson(res, 400, { error: "taskId query parameter is required" });
229
- return;
230
- }
231
- const taskDir = getTaskDir(config.projectRoot, taskId);
232
- if (!fs.existsSync(taskDir)) {
233
- sendJson(res, 404, { error: `Task not found: ${taskId}` });
234
- return;
235
- }
236
- const body = await readBody(req);
237
- const args = body.trim() ? JSON.parse(body) : {};
238
- const ctx = makeToolContext(taskId);
239
- console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${tool.name}`);
240
- const result = await tool.handler(args, ctx);
241
- console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${tool.name} done:`, JSON.stringify(result).slice(0, 200));
242
- sendJson(res, 200, result);
243
- } catch (err: any) {
244
- const status = err instanceof ToolError ? err.statusCode : 500;
245
- console.error(`[mcp] REST ${tool.name} error:`, err.message ?? String(err));
246
- sendJson(res, status, { error: err.message ?? String(err) });
247
- }
248
- return;
249
- }
250
-
251
- const matchedResource = req.method === "GET" && agentResources.find((r) => r.restPath === pathname);
252
- if (matchedResource) {
253
- if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
254
- const taskId = url.searchParams.get("taskId");
255
- if (!taskId) {
256
- sendJson(res, 400, { error: "taskId query parameter is required" });
257
- return;
258
- }
259
- const taskDir = getTaskDir(config.projectRoot, taskId);
260
- if (!fs.existsSync(taskDir)) {
261
- sendJson(res, 404, { error: `Task not found: ${taskId}` });
262
- return;
263
- }
264
- console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${matchedResource.name}`);
265
- const result = matchedResource.read();
266
- console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${matchedResource.name} done: ${JSON.stringify(result).slice(0, 200)}`);
267
- sendJson(res, 200, result);
268
- return;
269
- }
270
-
271
- if (req.method === "POST" && pathname === "/task-event/pop") {
272
- if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
273
- const taskId = url.searchParams.get("taskId");
274
- if (!taskId) {
275
- sendJson(res, 400, { error: "taskId query parameter is required" });
276
- return;
277
- }
278
- sendJson(res, 200, popEvent(taskId));
279
- return;
280
- }
281
-
282
- if (req.method === "POST" && pathname === "/event") {
283
- if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
284
- try {
285
- const body = await readBody(req);
286
- const event = JSON.parse(body);
287
- broadcastSseEvent(event);
288
- sendJson(res, 200, { ok: true });
289
- } catch { sendJson(res, 400, { error: "Invalid JSON" }); }
290
- return;
291
- }
292
-
293
- if (req.method === "POST" && pathname === "/pair-register") {
294
- if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
295
- try {
296
- const body = await readBody(req);
297
- const { code, expiryMs } = JSON.parse(body) as { code: string; expiryMs: number };
298
- if (!code) { sendJson(res, 400, { error: "Missing code" }); return; }
299
- if (pendingPairs.has(code)) { sendJson(res, 409, { error: "Code already registered" }); return; }
300
-
301
- const result = await new Promise<{ paired: boolean }>((resolve) => {
302
- const timer = setTimeout(() => {
303
- pendingPairs.delete(code);
304
- resolve({ paired: false });
305
- }, expiryMs ?? 60 * 1000);
306
-
307
- pendingPairs.set(code, { resolve, timer });
308
- req.on("close", () => {
309
- if (pendingPairs.has(code)) {
310
- clearTimeout(timer);
311
- pendingPairs.delete(code);
312
- }
313
- });
314
- });
315
-
316
- sendJson(res, 200, result);
317
- } catch { sendJson(res, 400, { error: "Invalid JSON" }); }
318
- return;
319
- }
320
-
321
- if (req.method === "POST" && pathname === "/request-permission") {
322
- if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
323
- try {
324
- const body = await readBody(req);
325
- const { taskId, taskName, permissions } = JSON.parse(body) as {
326
- taskId: string; taskName?: string; permissions: RequiredPermission[];
327
- };
328
- if (!taskId || !permissions?.length) {
329
- sendJson(res, 400, { error: "taskId and permissions are required" });
330
- return;
331
- }
332
-
333
- const pendingPromise = registerPending(taskId, "permission", permissions, {
334
- session_id: taskId,
335
- session_name: taskName,
336
- });
337
-
338
- await publishEvent(taskId, {
339
- event_type: "permission-request",
340
- host_id: config.hostId,
341
- required_permissions: permissions,
342
- session_name: taskName,
343
- });
344
-
345
- const response = await pendingPromise;
346
- const status = response[0] as "granted" | "granted_all" | "aborted";
347
-
348
- await publishEvent(taskId, {
349
- event_type: "permission-resolved",
350
- host_id: config.hostId,
351
- status,
352
- });
353
-
354
- sendJson(res, 200, { response: status });
355
- } catch (err) {
356
- sendJson(res, 500, { error: String(err) });
357
- }
358
- return;
359
- }
360
-
361
- if (req.method === "POST" && pathname === "/pair") {
362
- if (!isLocalhost(req)) { sendJson(res, 404, { error: "Not found" }); return; }
363
- try {
364
- const body = await readBody(req);
365
- const { code, label } = JSON.parse(body) as { code: string; label?: string };
366
- if (!code) { sendJson(res, 400, { error: "Missing code" }); return; }
367
-
368
- const pending = pendingPairs.get(code);
369
- if (!pending) { sendJson(res, 401, { error: "Invalid code" }); return; }
370
-
371
- const client = addClient(label);
372
- await persistDefaultInterface(config);
373
- const response: Record<string, unknown> = {
374
- hostId: config.hostId,
375
- clientToken: client.token,
376
- hostName: os.hostname(),
377
- };
378
-
379
- clearTimeout(pending.timer);
380
- pendingPairs.delete(code);
381
- pending.resolve({ paired: true });
382
-
383
- sendJson(res, 200, response);
384
- } catch { sendJson(res, 400, { error: "Invalid JSON" }); }
385
- return;
386
- }
387
-
388
- // Service worker and manifest require HTTPS, which loopback HTTP doesn't use.
389
- const SKIP = new Set(["/registerSW.js", "/service-worker.js", "/manifest.webmanifest"]);
390
-
391
- if (req.method === "GET" && pathname === "/events") {
392
- if (!isLocalhost(req)) { sendJson(res, 404, { error: "Not found" }); return; }
393
- res.writeHead(200, {
394
- "Content-Type": "text/event-stream",
395
- "Cache-Control": "no-cache",
396
- Connection: "keep-alive",
397
- });
398
- res.write(":ok\n\n");
399
-
400
- const heartbeat = setInterval(() => {
401
- res.write("data: {\"heartbeat\":true}\n\n");
402
- }, 5000);
403
-
404
- sseClients.add(res);
405
- req.on("close", () => {
406
- clearInterval(heartbeat);
407
- sseClients.delete(res);
408
- });
409
- return;
410
- }
411
-
412
- if (req.method === "POST" && pathname.startsWith("/rpc/")) {
413
- if (!isLocalhost(req) && !checkAuth(req)) {
414
- sendJson(res, 401, { error: "Unauthorized" });
415
- return;
416
- }
417
- const method = pathname.slice("/rpc/".length);
418
- if (!method) { sendJson(res, 400, { error: "Missing RPC method" }); return; }
419
-
420
- let params: Record<string, unknown> = {};
421
- try {
422
- const body = await readBody(req);
423
- if (body.trim().length > 0) {
424
- params = JSON.parse(body);
425
- }
426
- } catch { sendJson(res, 400, { error: "Invalid JSON" }); return; }
427
-
428
- const clientToken = extractClientToken(req);
429
- console.log(`[http] RPC: ${method}`);
430
-
431
- try {
432
- const response = await handleRpc({ method, params, clientToken, localhost: isLocalhost(req) });
433
- console.log(`[http] RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
434
- sendJson(res, 200, response);
435
- } catch (err) {
436
- console.error(`[http] RPC error (${method}):`, err);
437
- sendJson(res, 500, { error: String(err) });
438
- }
439
- return;
440
- }
441
-
442
- // PWA static assets — loopback only. Other devices must load the PWA from app.palmier.me.
443
- if (!isLocalhost(req)) { sendJson(res, 404, { error: "Not found" }); return; }
444
- if (SKIP.has(pathname)) { sendJson(res, 404, { error: "Not found" }); return; }
445
-
446
- let asset = getAsset(pathname);
447
- if (!asset && pathname !== "/") {
448
- asset = getAsset("/");
449
- }
450
-
451
- if (asset) {
452
- res.writeHead(200, { "Content-Type": asset.contentType });
453
- res.end(asset.data);
454
- } else {
455
- sendJson(res, 404, { error: "Not found" });
456
- }
457
- });
458
-
459
- return new Promise<void>((resolve, reject) => {
460
- server.listen(port, bindAddress, () => {
461
- console.log(`[http] Listening on ${bindAddress}:${port}`);
462
- onReady?.();
463
-
464
- const shutdown = () => {
465
- console.log("[http] Shutting down...");
466
- for (const client of sseClients) {
467
- client.end();
468
- }
469
- server.close(() => process.exit(0));
470
- };
471
-
472
- process.on("SIGINT", shutdown);
473
- process.on("SIGTERM", shutdown);
474
- });
475
-
476
- server.on("error", reject);
477
- });
478
- }
@@ -1,76 +0,0 @@
1
- import { StringCodec, type NatsConnection, type Msg, type Subscription } from "nats";
2
- import type { HostConfig, RpcMessage } from "../types.js";
3
-
4
- export async function startNatsTransport(
5
- config: HostConfig,
6
- handleRpc: (req: RpcMessage) => Promise<unknown>,
7
- nc: NatsConnection,
8
- ): Promise<void> {
9
- const sc = StringCodec();
10
-
11
- const subject = `host.${config.hostId}.rpc.>`;
12
- console.log(`[nats] Subscribing to: ${subject}`);
13
- const sub = nc.subscribe(subject);
14
-
15
- const shutdown = async () => {
16
- console.log("[nats] Shutting down...");
17
- sub.unsubscribe();
18
- await nc.drain();
19
- process.exit(0);
20
- };
21
-
22
- process.on("SIGINT", shutdown);
23
- process.on("SIGTERM", shutdown);
24
-
25
- async function processMessage(msg: Msg) {
26
- // Subject format: ...rpc.<method parts>
27
- const subjectTokens = msg.subject.split(".");
28
- const rpcIdx = subjectTokens.indexOf("rpc");
29
- const method = rpcIdx >= 0 ? subjectTokens.slice(rpcIdx + 1).join(".") : "";
30
-
31
- let params: Record<string, unknown> = {};
32
- if (msg.data && msg.data.length > 0) {
33
- const raw = sc.decode(msg.data).trim();
34
- if (raw.length > 0) {
35
- try {
36
- params = JSON.parse(raw);
37
- } catch {
38
- console.error(`[nats] Failed to parse RPC params for ${method}`);
39
- if (msg.reply) {
40
- msg.respond(sc.encode(JSON.stringify({ error: "Invalid JSON" })));
41
- }
42
- return;
43
- }
44
- }
45
- }
46
-
47
- // PWA includes the client token in the payload.
48
- const clientToken = typeof params.clientToken === "string" ? params.clientToken : undefined;
49
- delete params.clientToken;
50
-
51
- console.log(`[nats] RPC: ${method}`);
52
-
53
- let response: unknown;
54
- try {
55
- response = await handleRpc({ method, params, clientToken });
56
- } catch (err) {
57
- console.error(`[nats] RPC error (${method}):`, err);
58
- response = { error: String(err) };
59
- }
60
-
61
- console.log(`[nats] RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
62
- if (msg.reply) {
63
- msg.respond(sc.encode(JSON.stringify(response)));
64
- }
65
- }
66
-
67
- async function consumeSubscription(subscription: Subscription) {
68
- for await (const msg of subscription) {
69
- // Don't await — heartbeats must keep flowing while RPC runs.
70
- processMessage(msg);
71
- }
72
- }
73
-
74
- console.log("[nats] Waiting for RPC messages...");
75
- await consumeSubscription(sub);
76
- }
package/src/types.ts DELETED
@@ -1,89 +0,0 @@
1
- export interface HostConfig {
2
- hostId: string;
3
- projectRoot: string;
4
-
5
- natsUrl?: string;
6
- natsWsUrl?: string;
7
- natsJwt?: string;
8
- natsNkeySeed?: string;
9
-
10
- agents?: Array<{ key: string; label: string; supportsPermissions: boolean; supportsYolo: boolean }>;
11
-
12
- httpPort?: number;
13
-
14
- /** OS network interface name of the IPv4 default route, captured on the
15
- * most recent `palmier pair`. Used to resolve the host's LAN URL live for
16
- * `host.info` so the client can track IP changes (laptop roaming, DHCP). */
17
- defaultInterface?: string;
18
- }
19
-
20
- export interface TaskFrontmatter {
21
- id: string;
22
- name: string;
23
- user_prompt: string;
24
- agent: string;
25
- /**
26
- * Task schedule.
27
- * - `crons`: `schedule_values` holds cron expressions (e.g. "0 9 * * *")
28
- * - `specific_times`: `schedule_values` holds local datetime strings (e.g. "2026-04-20T09:00")
29
- * - `on_new_notification`: fires on each new Android notification from NATS. Optional `schedule_values` holds a single-entry packageName filter; empty/unset matches any app.
30
- * - `on_new_sms`: fires on each new SMS from NATS. Optional `schedule_values` holds a single-entry sender filter; compared after normalization (strip spaces/dashes/parens/plus, lowercase). Empty/unset matches any sender.
31
- */
32
- schedule_type?: "crons" | "specific_times" | "on_new_notification" | "on_new_sms";
33
- schedule_values?: string[];
34
- schedule_enabled: boolean;
35
- requires_confirmation: boolean;
36
- yolo_mode?: boolean;
37
- foreground_mode?: boolean;
38
- permissions?: RequiredPermission[];
39
- command?: string;
40
- /** Set when the task was created via task.run_oneoff. Used so the run process
41
- * can tear down its OS scheduler unit when it finishes — one-off tasks aren't
42
- * in tasks.jsonl so the daemon's recovery/sweep logic doesn't cover them. */
43
- one_off?: boolean;
44
- }
45
-
46
- export interface ParsedTask {
47
- frontmatter: TaskFrontmatter;
48
- }
49
-
50
- /**
51
- * - `started`: task is actively running
52
- * - `finished`: agent completed successfully
53
- * - `aborted`: user declined confirmation, permission, or input
54
- * - `failed`: agent exited with an error
55
- */
56
- export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
57
-
58
- export interface TaskStatus {
59
- running_state: TaskRunningState;
60
- time_stamp: number;
61
- /** PID of the palmier run process (used on Windows to kill the process tree). */
62
- pid?: number;
63
- }
64
-
65
- export interface HistoryEntry {
66
- task_id: string;
67
- run_id: string;
68
- }
69
-
70
- export interface RequiredPermission {
71
- name: string;
72
- description: string;
73
- }
74
-
75
- export interface ConversationMessage {
76
- role: "assistant" | "user" | "status";
77
- time: number;
78
- content: string;
79
- type?: "input" | "permission" | "confirmation" | "monitoring" | "started" | "finished" | "failed" | "aborted" | "stopped" | "error";
80
- attachments?: string[];
81
- }
82
-
83
- export interface RpcMessage {
84
- method: string;
85
- params: Record<string, unknown>;
86
- clientToken?: string;
87
- /** Trusted localhost request — skip client validation. */
88
- localhost?: boolean;
89
- }
@@ -1,40 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { fileURLToPath } from "url";
4
- import { spawnCommand } from "./spawn-command.js";
5
- import { getPlatform } from "./platform/index.js";
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
- const packageRoot = path.join(__dirname, "..");
9
- const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8")) as { version: string };
10
-
11
- /** True when running from a source checkout (has .git) rather than a global npm install. */
12
- export const isDevBuild = fs.existsSync(path.join(packageRoot, ".git"));
13
- export const currentVersion = isDevBuild ? `${pkg.version}-dev` : pkg.version;
14
-
15
- /** Returns an error message if the update fails. */
16
- export async function performUpdate(): Promise<string | null> {
17
- try {
18
- const { output, exitCode } = await spawnCommand("npm", ["update", "-g", "palmier"], {
19
- cwd: process.cwd(),
20
- timeout: 120_000,
21
- resolveOnFailure: true,
22
- });
23
- if (exitCode !== 0) {
24
- console.error(`[update] npm update failed (exit ${exitCode}):`, output);
25
- return `Update failed. Please run manually:\nnpm update -g palmier`;
26
- }
27
- console.log("[update] Update installed, restarting daemon...");
28
- // Delay so the RPC response finishes sending first.
29
- setTimeout(() => {
30
- getPlatform().restartDaemon().catch((err) => {
31
- console.error("[update] Restart failed:", err);
32
- });
33
- }, 1000);
34
- return null;
35
- } catch (err) {
36
- const msg = err instanceof Error ? err.message : String(err);
37
- console.error("[update] Update failed:", msg);
38
- return `Update failed. Please run manually:\nnpm update -g palmier`;
39
- }
40
- }