palmier 0.9.2 → 0.9.3

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 (30) hide show
  1. package/README.md +21 -20
  2. package/dist/platform/linux.js +14 -0
  3. package/dist/pwa/assets/index-BsB1tIsn.css +1 -0
  4. package/dist/pwa/assets/index-CknFGshO.js +120 -0
  5. package/dist/pwa/assets/{web-C2AU9S9n.js → web-DdzXb-jW.js} +1 -1
  6. package/dist/pwa/assets/{web-CfD_ah7K.js → web-Dl9aC-Qr.js} +1 -1
  7. package/dist/pwa/assets/{web-DugGj1t8.js → web-a9jK1xeo.js} +1 -1
  8. package/dist/pwa/index.html +3 -3
  9. package/dist/pwa/manifest.webmanifest +1 -1
  10. package/dist/pwa/service-worker.js +1 -1
  11. package/package.json +1 -1
  12. package/palmier-server/pwa/index.html +1 -1
  13. package/palmier-server/pwa/src/App.css +42 -81
  14. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +51 -9
  15. package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +5 -1
  16. package/palmier-server/pwa/src/components/HostMenu.tsx +10 -10
  17. package/palmier-server/pwa/src/components/{PlanDialog.tsx → PermissionsDialog.tsx} +6 -6
  18. package/palmier-server/pwa/src/components/RunDetailView.tsx +2 -0
  19. package/palmier-server/pwa/src/components/SessionsView.tsx +1 -0
  20. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +13 -3
  21. package/palmier-server/pwa/src/components/TaskForm.tsx +2 -2
  22. package/palmier-server/pwa/src/constants.ts +1 -1
  23. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +23 -8
  24. package/palmier-server/pwa/src/pages/PairHost.tsx +11 -4
  25. package/palmier-server/pwa/src/pages/PairSetup.tsx +8 -6
  26. package/palmier-server/pwa/vite.config.ts +1 -1
  27. package/palmier-server/spec.md +2 -2
  28. package/src/platform/linux.ts +9 -0
  29. package/dist/pwa/assets/index-BLCVzS_l.js +0 -120
  30. package/dist/pwa/assets/index-Cjjw24Ok.css +0 -1
@@ -1,2 +1,2 @@
1
1
  /** Bump when a breaking host change is made. */
2
- export const MIN_HOST_VERSION = "0.8.11";
2
+ export const MIN_HOST_VERSION = "0.9.2";
@@ -173,6 +173,14 @@ export function HostConnectionProvider({ children, activeHost }: { children: Rea
173
173
  config.natsJwt,
174
174
  new TextEncoder().encode(config.natsNkeySeed),
175
175
  ),
176
+ // Default is 2 min — too slow to notice a silently-dead WebSocket
177
+ // when the user has the page foregrounded but the network drops.
178
+ // The resume-probe useEffect below catches dead conns within 2s
179
+ // when the user returns to the app, so this background heartbeat
180
+ // can stay conservative (maxPingOut: 2 → ~60s) and avoid false
181
+ // positives from transient network jitter.
182
+ pingInterval: 30_000,
183
+ maxPingOut: 2,
176
184
  });
177
185
  if (cancelled) { conn.close().catch(() => {}); return; }
178
186
  console.log("[NATS] Connected");
@@ -211,17 +219,24 @@ export function HostConnectionProvider({ children, activeHost }: { children: Rea
211
219
  };
212
220
  }, [isDirectHost, activeHost]);
213
221
 
214
- // Mobile WebViews suspend WebSockets when the app backgrounds; nats.ws often
215
- // doesn't notice the dead socket on resume, leaving RPCs to hang/throw "Not
216
- // connected." Force-close on resume so the connectLoop reconnects with a
217
- // fresh socket.
222
+ // WebSockets can die silently mobile WebViews suspend them on background,
223
+ // laptops on sleep, networks on switch. On any resume (visibility change on
224
+ // web, foreground on native), probe the connection with a short-timeout
225
+ // flush (PING/PONG round-trip); close only if it doesn't respond. nats.ws's
226
+ // own pingInterval catches it eventually, but probing on resume gets us
227
+ // back faster when the user immediately fires an RPC.
218
228
  useEffect(() => {
219
229
  if (isDirectHost) return;
220
230
  const handle = CapacitorApp.addListener("appStateChange", (state: { isActive: boolean }) => {
221
- if (state.isActive && ncRef.current) {
222
- console.log("[NATS] App resumed; cycling NATS conn to recover from possible dead socket");
223
- ncRef.current.close().catch(() => {});
224
- }
231
+ if (!state.isActive || !ncRef.current) return;
232
+ const conn = ncRef.current;
233
+ Promise.race([
234
+ conn.flush(),
235
+ new Promise<void>((_, reject) => setTimeout(() => reject(new Error("ping timeout")), 2_000)),
236
+ ]).catch(() => {
237
+ console.log("[NATS] Resume probe failed — cycling conn");
238
+ conn.close().catch(() => {});
239
+ });
225
240
  });
226
241
  return () => { handle.then((h) => h.remove()); };
227
242
  }, [isDirectHost]);
@@ -141,8 +141,11 @@ export default function PairHost() {
141
141
  <h3 className="pair-instruction-heading">Setting up a new host?</h3>
142
142
  <ol className="pair-steps">
143
143
  <li>Install at least one agent CLI (e.g., <a href="https://www.palmier.me/agents" target="_blank" rel="noopener noreferrer">Claude Code, Gemini CLI, Codex CLI</a>)</li>
144
- <li>Install Palmier on your host machine:
145
- <code className="pair-command">npm install -g palmier</code>
144
+ <li>Install Palmier on your host machine.
145
+ <span className="pair-platform-label">Linux / macOS:</span>
146
+ <code className="pair-command">curl -fsSL https://palmier.me/install.sh | bash</code>
147
+ <span className="pair-platform-label">Windows (PowerShell):</span>
148
+ <code className="pair-command">irm https://palmier.me/install.ps1 | iex</code>
146
149
  </li>
147
150
  <li>Run the setup wizard:
148
151
  <code className="pair-command">palmier init</code>
@@ -189,8 +192,12 @@ export default function PairHost() {
189
192
  disabled={pairing}
190
193
  />
191
194
  <span className="pair-checkbox-text">
192
- <span className="pair-checkbox-title">Link this device</span>
193
- <span className="pair-checkbox-hint">The host will use this device for SMS, contacts, calendar, location, and alarms. Only one device can be linked at a time.</span>
195
+ <span className="pair-checkbox-title">Link the host to this device</span>
196
+ <span className="pair-checkbox-hint">
197
+ {makeLinked
198
+ ? "The host will use this device for SMS, contacts, calendar, location, and alarms. Only one device can be linked to the host."
199
+ : "This device won't provide SMS, contacts, calendar, location, or alarms to the host. You can link it later from the menu."}
200
+ </span>
194
201
  </span>
195
202
  </label>
196
203
  )}
@@ -74,9 +74,9 @@ export default function PairSetup() {
74
74
  const linkModal = phase === "confirming" && createPortal(
75
75
  <div className="confirm-modal-overlay" onClick={cancelLink}>
76
76
  <div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
77
- <h2 className="confirm-modal-title">Link this device?</h2>
77
+ <h2 className="confirm-modal-title">Link the host to this device?</h2>
78
78
  <p className="confirm-modal-message">
79
- Only one device can be linked at a time — switching will disable those capabilities on the currently linked device.
79
+ Another device is already linked to this host. Only one device can be linked to the host — switching will disable those capabilities on the currently linked device.
80
80
  </p>
81
81
  <div className="confirm-modal-actions">
82
82
  <button className="btn btn-secondary" onClick={cancelLink}>Cancel</button>
@@ -89,19 +89,21 @@ export default function PairSetup() {
89
89
 
90
90
  if (phase === "loading" || phase === "confirming" || phase === "linking" || phase === "linkError") {
91
91
  const isWizardCandidate = isFirstHost;
92
+ const showSpinner = phase === "loading" || phase === "confirming" || phase === "linking";
92
93
  return (
93
94
  <div className="pair-setup">
94
95
  <div className="pair-setup-inner">
95
96
  {isWizardCandidate && <h1 className="pair-setup-title">Device Capabilities</h1>}
96
97
  <div className="pair-setup-loading">
97
- {phase === "loading" && "Connecting to host…"}
98
- {phase === "confirming" && "Awaiting confirmation…"}
99
- {phase === "linking" && "Linking device…"}
98
+ {showSpinner && <span className="spinner spinner-lg" />}
99
+ {phase === "loading" && <span>Connecting to host…</span>}
100
+ {phase === "confirming" && <span>Awaiting confirmation…</span>}
101
+ {phase === "linking" && <span>Linking device…</span>}
100
102
  {phase === "linkError" && (
101
103
  <>
102
104
  <p className="pair-error">{linkError}</p>
103
105
  <button className="btn btn-primary" onClick={retryLink}>Retry</button>
104
- <button className="btn btn-secondary" onClick={goToHost} style={{ marginTop: 8 }}>Skip linking</button>
106
+ <button className="btn btn-secondary" onClick={goToHost}>Skip linking</button>
105
107
  </>
106
108
  )}
107
109
  </div>
@@ -24,7 +24,7 @@ export default defineConfig({
24
24
  manifest: {
25
25
  name: "Palmier",
26
26
  short_name: "Palmier",
27
- description: "Control AI agents running on your machine from any device. Schedule tasks, monitor runs, and stay in control.",
27
+ description: "Bridge your AI agents and your phone. Agents on your machine get your phone as a tool — SMS, calendar, GPS, alarms, approvals — and your phone as the remote to run them from anywhere.",
28
28
  start_url: "/",
29
29
  display: "standalone",
30
30
  background_color: "#ffffff",
@@ -144,8 +144,8 @@ If no clients exist, the host skips client validation (backward compatibility fo
144
144
 
145
145
  Each host tracks a single **linked device** — the one paired device responsible for answering device capability requests (SMS, contacts, calendar, location, alarm, ringer, email, battery). The host stores only `{ clientToken, fcmToken }` in `~/.config/palmier/linked-device.json`; it has no knowledge of which capabilities are actually enabled on the device. That set lives in Android SharedPreferences on the linked device itself and is consulted by the FCM handlers as a local kill-switch.
146
146
 
147
- - **Opt-in at pair time.** The PWA shows a "Link this device" checkbox during pairing (native only, default on). If checked, the pair flow continues to a setup step that calls `device.link` with the FCM token. On the very first host pair (when the device has no other paired hosts), that step also shows a one-time "Device Capabilities" screen with all toggles default OFF; the user opts into each one, and Finish writes the set to SharedPreferences. On subsequent host pairs the capability screen is skipped — the device-wide enabled set is host-agnostic and set once.
148
- - **Reassignment.** Any paired device can take over as the linked device from the drawer's "Link this device" button. This displaces the previous linked device (its drawer toggles go dark).
147
+ - **Opt-in at pair time.** The PWA shows a "Link the host to this device" checkbox during pairing (native only, default on). If checked, the pair flow continues to a setup step that calls `device.link` with the FCM token. On the very first host pair (when the device has no other paired hosts), that step also shows a one-time "Device Capabilities" screen with all toggles default OFF; the user opts into each one, and Finish writes the set to SharedPreferences. On subsequent host pairs the capability screen is skipped — the device-wide enabled set is host-agnostic and set once.
148
+ - **Reassignment.** Any paired device can take over as the linked device from the drawer's "Link the host to this device" button. This displaces the previous linked device (its drawer toggles go dark).
149
149
  - **Loss.** If the linked device is unpaired (via `clients.revoke_self` or CLI `palmier clients revoke`), `linked-device.json` is cleared and capability tools return "No linked device configured" until the user picks a new one.
150
150
  - **Routing.** MCP capability tools look up the linked device once per invocation and publish FCM to its token (via `host.<host_id>.fcm.<capability>` relayed by the server). Non-linked devices aren't woken and don't receive capability FCMs.
151
151
 
@@ -185,6 +185,15 @@ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
185
185
  `;
186
186
 
187
187
  fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
188
+
189
+ // Tear down any previously installed timer unit so on-demand tasks don't
190
+ // keep firing on the old schedule. Service unit stays so startTask works.
191
+ const timerPath = path.join(UNIT_DIR, timerName);
192
+ if (fs.existsSync(timerPath)) {
193
+ try { execSync(`systemctl --user stop ${timerName}`, { encoding: "utf-8" }); } catch { /* not running */ }
194
+ try { execSync(`systemctl --user disable ${timerName}`, { encoding: "utf-8" }); } catch { /* not enabled */ }
195
+ fs.unlinkSync(timerPath);
196
+ }
188
197
  daemonReload();
189
198
 
190
199
  if (!task.frontmatter.schedule_enabled) return;