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,688 +0,0 @@
1
- import "dotenv/config";
2
- import fs from "fs";
3
- import path from "path";
4
- import express from "express";
5
- import helmet from "helmet";
6
- import { pool, initDb } from "./db.js";
7
- import { connectNats, getNatsConnection } from "./nats.js";
8
- import { StringCodec } from "nats";
9
-
10
- import hostsRoutes from "./routes/hosts.js";
11
- import pushRoutes from "./routes/push.js";
12
- import fcmRoutes from "./routes/fcm.js";
13
- import deviceRoutes from "./routes/device.js";
14
- import { notifyClients } from "./notify.js";
15
- import { sendFcmToClients, sendFcmToDevice } from "./fcm.js";
16
- import { createPairingCredentials, createPwaCredentials } from "./nats-jwt.js";
17
-
18
- const PORT = parseInt(process.env.PORT || "3000", 10);
19
-
20
- async function main(): Promise<void> {
21
- // Initialize database tables
22
- await initDb();
23
-
24
- // Connect to NATS
25
- await connectNats();
26
- const sc = StringCodec();
27
-
28
- // Subscribe to unified host-event pub/sub for push notifications
29
- (async () => {
30
- try {
31
- const conn = await getNatsConnection();
32
- const sub = conn.subscribe("host-event.>");
33
- console.log("Listening for host-event notifications");
34
-
35
- for await (const msg of sub) {
36
- try {
37
- // Subject: host-event.<host_id>.<task_id>
38
- const tokens = msg.subject.split(".");
39
- if (tokens.length < 3) continue;
40
- const hostId = tokens[1];
41
- const taskId = tokens.slice(2).join(".");
42
-
43
- const data = JSON.parse(sc.decode(msg.data)) as {
44
- event_type: string;
45
- running_state?: string;
46
- name?: string;
47
- run_id?: string;
48
- session_id?: string;
49
- description?: string;
50
- agent_name?: string;
51
- required_permissions?: Array<{ name: string; description: string }>;
52
- input_questions?: string[];
53
- result_file?: string;
54
- };
55
-
56
- if (data.event_type === "confirm-request") {
57
- await notifyClients(hostId, {
58
- type: "confirm",
59
- title: "Confirmation Required",
60
- body: data.description || "A task requires confirmation to run.",
61
- host_id: hostId,
62
- session_id: data.session_id,
63
- agent_name: data.agent_name,
64
- });
65
- } else if (data.event_type === "confirm-resolved") {
66
- await notifyClients(hostId, {
67
- type: "confirm-dismiss",
68
- host_id: hostId,
69
- session_id: data.session_id,
70
- });
71
- } else if (data.event_type === "permission-request") {
72
- const taskLabel = data.name
73
- ? data.name.length > 60 ? data.name.slice(0, 60) + "…" : data.name
74
- : "A task";
75
- await notifyClients(hostId, {
76
- type: "permission",
77
- title: "Permission Required",
78
- body: `${taskLabel} needs additional permissions to continue.`,
79
- task_id: taskId,
80
- host_id: hostId,
81
- });
82
- } else if (data.event_type === "permission-resolved") {
83
- await notifyClients(hostId, {
84
- type: "permission-dismiss",
85
- task_id: taskId,
86
- host_id: hostId,
87
- });
88
- } else if (data.event_type === "input-request") {
89
- await notifyClients(hostId, {
90
- type: "input",
91
- title: "Input Required",
92
- body: "A task needs your input to continue.",
93
- host_id: hostId,
94
- session_id: data.session_id,
95
- agent_name: data.agent_name,
96
- });
97
- } else if (data.event_type === "input-resolved") {
98
- await notifyClients(hostId, {
99
- type: "input-dismiss",
100
- host_id: hostId,
101
- session_id: data.session_id,
102
- });
103
- } else if (data.event_type === "report-generated" || (data.event_type === "running-state" && data.running_state === "failed")) {
104
- const label = data.name;
105
- const taskLabel = label
106
- ? label.length > 60 ? label.slice(0, 60) + "…" : label
107
- : "Task";
108
- const isFailure = data.running_state === "failed";
109
- const body = isFailure ? `${taskLabel} — failed` : `${taskLabel} — report ready`;
110
- await notifyClients(hostId, {
111
- type: isFailure ? "fail" : "complete",
112
- title: "Palmier",
113
- body,
114
- task_id: taskId,
115
- host_id: hostId,
116
- run_id: data.run_id,
117
- result_file: data.result_file,
118
- });
119
- }
120
- } catch (err) {
121
- console.error("[host-event→Push] Error:", err);
122
- }
123
- }
124
- } catch (err) {
125
- console.error("Failed to subscribe to host-event:", err);
126
- }
127
- })();
128
-
129
- // Subscribe to push notification requests from hosts
130
- (async () => {
131
- try {
132
- const conn = await getNatsConnection();
133
- const sub = conn.subscribe("host.*.push.send");
134
- console.log("Listening for push notification requests");
135
-
136
- for await (const msg of sub) {
137
- try {
138
- const data = JSON.parse(sc.decode(msg.data)) as {
139
- hostId: string;
140
- title: string;
141
- body: string;
142
- session_id?: string;
143
- agent_name?: string;
144
- };
145
-
146
- // Validate hostId in subject matches payload
147
- const subjectHostId = msg.subject.split(".")[1];
148
- if (data.hostId !== subjectHostId) {
149
- if (msg.reply) {
150
- msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
151
- }
152
- continue;
153
- }
154
-
155
- // If no agent_name, session_id is a taskId — include as task_id for deep-linking
156
- const isTask = data.session_id && !data.agent_name;
157
-
158
- console.log(`[Push] Sending notification for host ${data.hostId}`);
159
- await notifyClients(data.hostId, {
160
- type: "notification",
161
- host_id: data.hostId,
162
- title: data.title,
163
- body: data.body,
164
- ...(isTask ? { task_id: data.session_id } : {}),
165
- agent_name: data.agent_name,
166
- });
167
-
168
- if (msg.reply) {
169
- msg.respond(sc.encode(JSON.stringify({ ok: true })));
170
- }
171
- } catch (err) {
172
- console.error("[Push] Error sending notification:", err);
173
- if (msg.reply) {
174
- msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
175
- }
176
- }
177
- }
178
- } catch (err) {
179
- console.error("Failed to subscribe to push requests:", err);
180
- }
181
- })();
182
-
183
- // Subscribe to FCM geolocation requests from hosts
184
- (async () => {
185
- try {
186
- const conn = await getNatsConnection();
187
- const sub = conn.subscribe("host.*.fcm.geolocation");
188
- console.log("Listening for FCM geolocation requests");
189
-
190
- for await (const msg of sub) {
191
- try {
192
- const data = JSON.parse(sc.decode(msg.data)) as {
193
- hostId: string;
194
- requestId: string;
195
- fcmToken?: string;
196
- };
197
-
198
- const subjectHostId = msg.subject.split(".")[1];
199
- if (data.hostId !== subjectHostId) {
200
- if (msg.reply) {
201
- msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
202
- }
203
- continue;
204
- }
205
-
206
- const fcmPayload = {
207
- type: "geolocation-request",
208
- requestId: data.requestId,
209
- hostId: data.hostId,
210
- };
211
-
212
- console.log(`[FCM] Sending geolocation request for host ${data.hostId}`);
213
- if (data.fcmToken) {
214
- await sendFcmToDevice(data.fcmToken, fcmPayload);
215
- } else {
216
- await sendFcmToClients(data.hostId, fcmPayload);
217
- }
218
-
219
- if (msg.reply) {
220
- msg.respond(sc.encode(JSON.stringify({ ok: true })));
221
- }
222
- } catch (err) {
223
- console.error("[FCM] Error handling geolocation request:", err);
224
- if (msg.reply) {
225
- msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
226
- }
227
- }
228
- }
229
- } catch (err) {
230
- console.error("Failed to subscribe to FCM geolocation requests:", err);
231
- }
232
- })();
233
-
234
- // Subscribe to contacts requests from hosts
235
- (async () => {
236
- try {
237
- const conn = await getNatsConnection();
238
- const sub = conn.subscribe("host.*.fcm.contacts");
239
- console.log("Listening for FCM contacts requests");
240
-
241
- for await (const msg of sub) {
242
- try {
243
- const data = JSON.parse(sc.decode(msg.data)) as {
244
- hostId: string;
245
- requestId: string;
246
- fcmToken?: string;
247
- action: string;
248
- name?: string;
249
- phone?: string;
250
- email?: string;
251
- };
252
-
253
- const subjectHostId = msg.subject.split(".")[1];
254
- if (data.hostId !== subjectHostId) {
255
- if (msg.reply) {
256
- msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
257
- }
258
- continue;
259
- }
260
-
261
- const fcmPayload: Record<string, string> = {
262
- type: data.action === "create" ? "create-contact" : "read-contacts",
263
- requestId: data.requestId,
264
- hostId: data.hostId,
265
- };
266
- if (data.name) fcmPayload.name = data.name;
267
- if (data.phone) fcmPayload.phone = data.phone;
268
- if (data.email) fcmPayload.email = data.email;
269
-
270
- console.log(`[FCM] Sending contacts ${data.action} request for host ${data.hostId}`);
271
- if (data.fcmToken) {
272
- await sendFcmToDevice(data.fcmToken, fcmPayload);
273
- } else {
274
- await sendFcmToClients(data.hostId, fcmPayload);
275
- }
276
-
277
- if (msg.reply) {
278
- msg.respond(sc.encode(JSON.stringify({ ok: true })));
279
- }
280
- } catch (err) {
281
- console.error("[FCM] Error handling contacts request:", err);
282
- if (msg.reply) {
283
- msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
284
- }
285
- }
286
- }
287
- } catch (err) {
288
- console.error("Failed to subscribe to FCM contacts requests:", err);
289
- }
290
- })();
291
-
292
- // Subscribe to calendar requests from hosts
293
- (async () => {
294
- try {
295
- const conn = await getNatsConnection();
296
- const sub = conn.subscribe("host.*.fcm.calendar");
297
- console.log("Listening for FCM calendar requests");
298
-
299
- for await (const msg of sub) {
300
- try {
301
- const data = JSON.parse(sc.decode(msg.data)) as {
302
- hostId: string;
303
- requestId: string;
304
- action: string;
305
- [key: string]: string;
306
- };
307
-
308
- const subjectHostId = msg.subject.split(".")[1];
309
- if (data.hostId !== subjectHostId) {
310
- if (msg.reply) {
311
- msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
312
- }
313
- continue;
314
- }
315
-
316
- const fcmPayload: Record<string, string> = {
317
- type: data.action === "create" ? "create-calendar-event" : "read-calendar",
318
- requestId: data.requestId,
319
- hostId: data.hostId,
320
- };
321
- // Forward optional fields
322
- for (const key of ["startDate", "endDate", "title", "startTime", "endTime", "location", "description"]) {
323
- if (data[key]) fcmPayload[key] = data[key];
324
- }
325
-
326
- console.log(`[FCM] Sending calendar ${data.action} request for host ${data.hostId}`);
327
- await sendFcmToClients(data.hostId, fcmPayload);
328
-
329
- if (msg.reply) {
330
- msg.respond(sc.encode(JSON.stringify({ ok: true })));
331
- }
332
- } catch (err) {
333
- console.error("[FCM] Error handling calendar request:", err);
334
- if (msg.reply) {
335
- msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
336
- }
337
- }
338
- }
339
- } catch (err) {
340
- console.error("Failed to subscribe to FCM calendar requests:", err);
341
- }
342
- })();
343
-
344
- // Subscribe to send-SMS requests from hosts
345
- (async () => {
346
- try {
347
- const conn = await getNatsConnection();
348
- const sub = conn.subscribe("host.*.fcm.sms");
349
- console.log("Listening for FCM SMS requests");
350
-
351
- for await (const msg of sub) {
352
- try {
353
- const data = JSON.parse(sc.decode(msg.data)) as {
354
- hostId: string;
355
- requestId: string;
356
- action: string;
357
- to?: string;
358
- body?: string;
359
- };
360
-
361
- const subjectHostId = msg.subject.split(".")[1];
362
- if (data.hostId !== subjectHostId) {
363
- if (msg.reply) {
364
- msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
365
- }
366
- continue;
367
- }
368
-
369
- const fcmPayload: Record<string, string> = {
370
- type: "send-sms",
371
- requestId: data.requestId,
372
- hostId: data.hostId,
373
- };
374
- if (data.to) fcmPayload.to = data.to;
375
- if (data.body) fcmPayload.body = data.body;
376
-
377
- console.log(`[FCM] Sending SMS request for host ${data.hostId}`);
378
- await sendFcmToClients(data.hostId, fcmPayload);
379
-
380
- if (msg.reply) {
381
- msg.respond(sc.encode(JSON.stringify({ ok: true })));
382
- }
383
- } catch (err) {
384
- console.error("[FCM] Error handling SMS request:", err);
385
- if (msg.reply) {
386
- msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
387
- }
388
- }
389
- }
390
- } catch (err) {
391
- console.error("Failed to subscribe to FCM SMS requests:", err);
392
- }
393
- })();
394
-
395
- // Subscribe to alarm requests from hosts
396
- (async () => {
397
- try {
398
- const conn = await getNatsConnection();
399
- const sub = conn.subscribe("host.*.fcm.alarm");
400
- console.log("Listening for FCM alarm requests");
401
-
402
- for await (const msg of sub) {
403
- try {
404
- const data = JSON.parse(sc.decode(msg.data)) as {
405
- hostId: string;
406
- requestId: string;
407
- fcmToken?: string;
408
- title?: string;
409
- description?: string;
410
- };
411
-
412
- const subjectHostId = msg.subject.split(".")[1];
413
- if (data.hostId !== subjectHostId) {
414
- if (msg.reply) {
415
- msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
416
- }
417
- continue;
418
- }
419
-
420
- const fcmPayload: Record<string, string> = {
421
- type: "send-alarm",
422
- requestId: data.requestId,
423
- hostId: data.hostId,
424
- };
425
- if (data.title) fcmPayload.title = data.title;
426
- if (data.description) fcmPayload.description = data.description;
427
-
428
- console.log(`[FCM] Sending alarm request for host ${data.hostId}`);
429
- if (data.fcmToken) {
430
- await sendFcmToDevice(data.fcmToken, fcmPayload);
431
- } else {
432
- await sendFcmToClients(data.hostId, fcmPayload);
433
- }
434
-
435
- if (msg.reply) {
436
- msg.respond(sc.encode(JSON.stringify({ ok: true })));
437
- }
438
- } catch (err) {
439
- console.error("[FCM] Error handling alarm request:", err);
440
- if (msg.reply) {
441
- msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
442
- }
443
- }
444
- }
445
- } catch (err) {
446
- console.error("Failed to subscribe to FCM alarm requests:", err);
447
- }
448
- })();
449
-
450
- // Subscribe to email requests from hosts
451
- (async () => {
452
- try {
453
- const conn = await getNatsConnection();
454
- const sub = conn.subscribe("host.*.fcm.email");
455
- console.log("Listening for FCM email requests");
456
-
457
- for await (const msg of sub) {
458
- try {
459
- const data = JSON.parse(sc.decode(msg.data)) as {
460
- hostId: string;
461
- requestId: string;
462
- fcmToken?: string;
463
- [key: string]: string | undefined;
464
- };
465
-
466
- const subjectHostId = msg.subject.split(".")[1];
467
- if (data.hostId !== subjectHostId) {
468
- if (msg.reply) {
469
- msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
470
- }
471
- continue;
472
- }
473
-
474
- const fcmPayload: Record<string, string> = {
475
- type: "send-email",
476
- requestId: data.requestId,
477
- hostId: data.hostId,
478
- };
479
- for (const key of ["to", "subject", "body", "cc", "bcc"]) {
480
- if (data[key]) fcmPayload[key] = data[key]!;
481
- }
482
-
483
- console.log(`[FCM] Sending email request for host ${data.hostId}`);
484
- if (data.fcmToken) {
485
- await sendFcmToDevice(data.fcmToken, fcmPayload);
486
- } else {
487
- await sendFcmToClients(data.hostId, fcmPayload);
488
- }
489
-
490
- if (msg.reply) {
491
- msg.respond(sc.encode(JSON.stringify({ ok: true })));
492
- }
493
- } catch (err) {
494
- console.error("[FCM] Error handling email request:", err);
495
- if (msg.reply) {
496
- msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
497
- }
498
- }
499
- }
500
- } catch (err) {
501
- console.error("Failed to subscribe to FCM email requests:", err);
502
- }
503
- })();
504
-
505
- // Subscribe to battery requests from hosts
506
- (async () => {
507
- try {
508
- const conn = await getNatsConnection();
509
- const sub = conn.subscribe("host.*.fcm.battery");
510
- console.log("Listening for FCM battery requests");
511
-
512
- for await (const msg of sub) {
513
- try {
514
- const data = JSON.parse(sc.decode(msg.data)) as {
515
- hostId: string;
516
- requestId: string;
517
- };
518
-
519
- const subjectHostId = msg.subject.split(".")[1];
520
- if (data.hostId !== subjectHostId) {
521
- if (msg.reply) {
522
- msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
523
- }
524
- continue;
525
- }
526
-
527
- console.log(`[FCM] Sending battery request for host ${data.hostId}`);
528
- await sendFcmToClients(data.hostId, {
529
- type: "read-battery",
530
- requestId: data.requestId,
531
- hostId: data.hostId,
532
- });
533
-
534
- if (msg.reply) {
535
- msg.respond(sc.encode(JSON.stringify({ ok: true })));
536
- }
537
- } catch (err) {
538
- console.error("[FCM] Error handling battery request:", err);
539
- if (msg.reply) {
540
- msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
541
- }
542
- }
543
- }
544
- } catch (err) {
545
- console.error("Failed to subscribe to FCM battery requests:", err);
546
- }
547
- })();
548
-
549
- // Subscribe to ringer mode requests from hosts
550
- (async () => {
551
- try {
552
- const conn = await getNatsConnection();
553
- const sub = conn.subscribe("host.*.fcm.ringer");
554
- console.log("Listening for FCM ringer requests");
555
-
556
- for await (const msg of sub) {
557
- try {
558
- const data = JSON.parse(sc.decode(msg.data)) as {
559
- hostId: string;
560
- requestId: string;
561
- mode: string;
562
- };
563
-
564
- const subjectHostId = msg.subject.split(".")[1];
565
- if (data.hostId !== subjectHostId) {
566
- if (msg.reply) {
567
- msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
568
- }
569
- continue;
570
- }
571
-
572
- console.log(`[FCM] Sending ringer mode request for host ${data.hostId}`);
573
- await sendFcmToClients(data.hostId, {
574
- type: "set-ringer-mode",
575
- requestId: data.requestId,
576
- hostId: data.hostId,
577
- mode: data.mode,
578
- });
579
-
580
- if (msg.reply) {
581
- msg.respond(sc.encode(JSON.stringify({ ok: true })));
582
- }
583
- } catch (err) {
584
- console.error("[FCM] Error handling ringer request:", err);
585
- if (msg.reply) {
586
- msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
587
- }
588
- }
589
- }
590
- } catch (err) {
591
- console.error("Failed to subscribe to FCM ringer requests:", err);
592
- }
593
- })();
594
-
595
- // Create Express app
596
- const app = express();
597
-
598
- app.use(
599
- helmet({
600
- contentSecurityPolicy: false,
601
- crossOriginResourcePolicy: false,
602
- })
603
- );
604
-
605
- // CORS for Capacitor Android app (requests from capacitor://localhost)
606
- app.use((req, res, next) => {
607
- const origin = req.headers.origin;
608
- if (origin && (origin.startsWith("capacitor://") || origin === "https://localhost" || origin.startsWith("http://localhost"))) {
609
- res.setHeader("Access-Control-Allow-Origin", origin);
610
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
611
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
612
- }
613
- if (req.method === "OPTIONS") {
614
- res.sendStatus(204);
615
- return;
616
- }
617
- next();
618
- });
619
-
620
- app.use(express.json());
621
-
622
- // Mount routes
623
- app.use("/api/hosts", hostsRoutes);
624
- app.use("/api/push", pushRoutes);
625
- app.use("/api/fcm", fcmRoutes);
626
- app.use("/api/device", deviceRoutes);
627
-
628
- // Public NATS config endpoint — returns pairing-only credentials.
629
- // These can only publish to pair.* subjects (no RPC, no event subscriptions).
630
- app.get("/api/config", (_req, res) => {
631
- const accountSeed = process.env.NATS_ACCOUNT_SEED;
632
- if (!accountSeed) {
633
- res.status(500).json({ error: "Server NATS auth not configured" });
634
- return;
635
- }
636
- const creds = createPairingCredentials(accountSeed);
637
- res.json({
638
- natsWsUrl: process.env.NATS_WS_URL || "",
639
- natsJwt: creds.jwt,
640
- natsNkeySeed: creds.nkeySeed,
641
- });
642
- });
643
-
644
- // Host-scoped NATS credentials — returns JWT scoped to a single host's subjects.
645
- // Called by the PWA after pairing to get credentials for RPC + event subscriptions.
646
- app.get("/api/nats-credentials/:hostId", (req, res) => {
647
- const accountSeed = process.env.NATS_ACCOUNT_SEED;
648
- if (!accountSeed) {
649
- res.status(500).json({ error: "Server NATS auth not configured" });
650
- return;
651
- }
652
- const { hostId } = req.params;
653
- const creds = createPwaCredentials(accountSeed, hostId);
654
- res.json({
655
- natsWsUrl: process.env.NATS_WS_URL || "",
656
- natsJwt: creds.jwt,
657
- natsNkeySeed: creds.nkeySeed,
658
- });
659
- });
660
-
661
- // Health check
662
- app.get("/health", async (_req, res) => {
663
- try {
664
- await pool.query("SELECT 1");
665
- res.json({ status: "ok" });
666
- } catch {
667
- res.status(503).json({ status: "unhealthy" });
668
- }
669
- });
670
-
671
- // Serve built PWA static files (production only)
672
- const pwaDistPath = path.resolve(process.cwd(), "../pwa/dist");
673
- if (fs.existsSync(pwaDistPath)) {
674
- app.use(express.static(pwaDistPath));
675
- app.get("*", (_req, res) => {
676
- res.sendFile(path.join(pwaDistPath, "index.html"));
677
- });
678
- }
679
-
680
- app.listen(PORT, () => {
681
- console.log(`Palmier server listening on port ${PORT}`);
682
- });
683
- }
684
-
685
- main().catch((err) => {
686
- console.error("Failed to start server:", err);
687
- process.exit(1);
688
- });