palmier 0.9.6 → 0.9.7

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 (250) 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 +0 -1
  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/pwa/service-worker.js +1 -1
  96. package/dist/rpc-handler.d.ts +0 -1
  97. package/dist/rpc-handler.js +0 -1
  98. package/dist/sms-store.d.ts +0 -1
  99. package/dist/sms-store.js +0 -1
  100. package/dist/spawn-command.d.ts +0 -1
  101. package/dist/spawn-command.js +0 -1
  102. package/dist/task.d.ts +0 -1
  103. package/dist/task.js +0 -1
  104. package/dist/transports/http-transport.d.ts +0 -1
  105. package/dist/transports/http-transport.js +0 -1
  106. package/dist/transports/nats-transport.d.ts +0 -1
  107. package/dist/transports/nats-transport.js +0 -1
  108. package/dist/types.d.ts +0 -1
  109. package/dist/types.js +0 -1
  110. package/dist/update-checker.d.ts +0 -1
  111. package/dist/update-checker.js +0 -1
  112. package/package.json +5 -1
  113. package/.github/workflows/ci.yml +0 -16
  114. package/.github/workflows/publish.yml +0 -37
  115. package/CLAUDE.md +0 -22
  116. package/palmier-server/.github/workflows/ci.yml +0 -21
  117. package/palmier-server/.github/workflows/deploy.yml +0 -38
  118. package/palmier-server/CLAUDE.md +0 -17
  119. package/palmier-server/PRODUCTION.md +0 -358
  120. package/palmier-server/README.md +0 -231
  121. package/palmier-server/nats.conf +0 -19
  122. package/palmier-server/package.json +0 -15
  123. package/palmier-server/pnpm-lock.yaml +0 -7639
  124. package/palmier-server/pnpm-workspace.yaml +0 -3
  125. package/palmier-server/pwa/index.html +0 -16
  126. package/palmier-server/pwa/logo/logo_20260421.png +0 -0
  127. package/palmier-server/pwa/package.json +0 -34
  128. package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
  129. package/palmier-server/pwa/public/favicon.ico +0 -0
  130. package/palmier-server/pwa/public/pwa-192x192.png +0 -0
  131. package/palmier-server/pwa/public/pwa-512x512.png +0 -0
  132. package/palmier-server/pwa/src/App.css +0 -3012
  133. package/palmier-server/pwa/src/App.tsx +0 -59
  134. package/palmier-server/pwa/src/agentLabels.ts +0 -11
  135. package/palmier-server/pwa/src/api.ts +0 -67
  136. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
  137. package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -113
  138. package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
  139. package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
  140. package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
  141. package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
  142. package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
  143. package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
  144. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
  145. package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
  146. package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
  147. package/palmier-server/pwa/src/components/TaskForm.tsx +0 -766
  148. package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
  149. package/palmier-server/pwa/src/constants.ts +0 -2
  150. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
  151. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
  152. package/palmier-server/pwa/src/draftGuard.ts +0 -24
  153. package/palmier-server/pwa/src/formatTime.ts +0 -44
  154. package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
  155. package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
  156. package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
  157. package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
  158. package/palmier-server/pwa/src/main.tsx +0 -14
  159. package/palmier-server/pwa/src/native/Device.ts +0 -49
  160. package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
  161. package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
  162. package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
  163. package/palmier-server/pwa/src/service-worker.ts +0 -142
  164. package/palmier-server/pwa/src/types.ts +0 -75
  165. package/palmier-server/pwa/src/vite-env.d.ts +0 -11
  166. package/palmier-server/pwa/tsconfig.json +0 -21
  167. package/palmier-server/pwa/tsconfig.node.json +0 -19
  168. package/palmier-server/pwa/vite.config.ts +0 -47
  169. package/palmier-server/server/.env.example +0 -20
  170. package/palmier-server/server/package.json +0 -36
  171. package/palmier-server/server/src/db.ts +0 -44
  172. package/palmier-server/server/src/fcm.ts +0 -74
  173. package/palmier-server/server/src/index.ts +0 -688
  174. package/palmier-server/server/src/nats-jwt.ts +0 -299
  175. package/palmier-server/server/src/nats-setup.ts +0 -48
  176. package/palmier-server/server/src/nats.ts +0 -33
  177. package/palmier-server/server/src/notify.ts +0 -34
  178. package/palmier-server/server/src/push.ts +0 -68
  179. package/palmier-server/server/src/routes/device.ts +0 -224
  180. package/palmier-server/server/src/routes/fcm.ts +0 -64
  181. package/palmier-server/server/src/routes/hosts.ts +0 -56
  182. package/palmier-server/server/src/routes/push.ts +0 -101
  183. package/palmier-server/server/tsconfig.json +0 -20
  184. package/palmier-server/spec.md +0 -533
  185. package/src/agents/agent-instructions.md +0 -28
  186. package/src/agents/agent.ts +0 -114
  187. package/src/agents/aider.ts +0 -35
  188. package/src/agents/claude.ts +0 -39
  189. package/src/agents/cline.ts +0 -35
  190. package/src/agents/codex.ts +0 -40
  191. package/src/agents/copilot.ts +0 -37
  192. package/src/agents/cursor.ts +0 -36
  193. package/src/agents/deepagents.ts +0 -36
  194. package/src/agents/droid.ts +0 -35
  195. package/src/agents/gemini.ts +0 -43
  196. package/src/agents/goose.ts +0 -33
  197. package/src/agents/hermes.ts +0 -36
  198. package/src/agents/kimi.ts +0 -35
  199. package/src/agents/kiro.ts +0 -36
  200. package/src/agents/openclaw.ts +0 -29
  201. package/src/agents/opencode.ts +0 -36
  202. package/src/agents/qoder.ts +0 -36
  203. package/src/agents/qwen.ts +0 -32
  204. package/src/agents/shared-prompt.ts +0 -30
  205. package/src/client-store.ts +0 -68
  206. package/src/commands/clients.ts +0 -29
  207. package/src/commands/info.ts +0 -29
  208. package/src/commands/init.ts +0 -165
  209. package/src/commands/pair.ts +0 -137
  210. package/src/commands/restart.ts +0 -6
  211. package/src/commands/run.ts +0 -608
  212. package/src/commands/serve.ts +0 -211
  213. package/src/commands/uninstall.ts +0 -9
  214. package/src/config.ts +0 -36
  215. package/src/cross-spawn.d.ts +0 -5
  216. package/src/event-queues.ts +0 -41
  217. package/src/events.ts +0 -29
  218. package/src/index.ts +0 -111
  219. package/src/linked-device.ts +0 -52
  220. package/src/mcp-handler.ts +0 -200
  221. package/src/mcp-tools.ts +0 -839
  222. package/src/nats-client.ts +0 -19
  223. package/src/network.ts +0 -96
  224. package/src/notification-store.ts +0 -30
  225. package/src/pending-requests.ts +0 -73
  226. package/src/platform/index.ts +0 -20
  227. package/src/platform/linux.ts +0 -296
  228. package/src/platform/macos.ts +0 -329
  229. package/src/platform/platform.ts +0 -31
  230. package/src/platform/windows.ts +0 -299
  231. package/src/rpc-handler.ts +0 -691
  232. package/src/sms-store.ts +0 -28
  233. package/src/spawn-command.ts +0 -123
  234. package/src/task.ts +0 -343
  235. package/src/transports/http-transport.ts +0 -478
  236. package/src/transports/nats-transport.ts +0 -76
  237. package/src/types.ts +0 -89
  238. package/src/update-checker.ts +0 -40
  239. package/test/agent-instructions.test.ts +0 -209
  240. package/test/agent-output-parsing.test.ts +0 -74
  241. package/test/linux-cron.test.ts +0 -41
  242. package/test/macos-plist.test.ts +0 -112
  243. package/test/notification-store.test.ts +0 -57
  244. package/test/pairing.test.ts +0 -35
  245. package/test/result-state.test.ts +0 -110
  246. package/test/task-parsing.test.ts +0 -82
  247. package/test/taskrun-messages.test.ts +0 -224
  248. package/test/tsconfig.json +0 -9
  249. package/test/windows-xml.test.ts +0 -89
  250. 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
- });