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