palmier 0.8.3 → 0.8.4

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 (39) hide show
  1. package/README.md +6 -4
  2. package/dist/commands/pair.js +2 -0
  3. package/dist/commands/serve.js +0 -4
  4. package/dist/platform/index.js +7 -3
  5. package/dist/platform/macos.d.ts +32 -0
  6. package/dist/platform/macos.js +287 -0
  7. package/dist/pwa/assets/index-499vYQvR.js +120 -0
  8. package/dist/pwa/assets/{index-B0F9mtid.css → index-UaZFu6XL.css} +1 -1
  9. package/dist/pwa/assets/{web-Z1623me-.js → web-Bp48ONY3.js} +1 -1
  10. package/dist/pwa/assets/{web-C6lkQj9J.js → web-CyJutAy4.js} +1 -1
  11. package/dist/pwa/index.html +2 -2
  12. package/dist/pwa/service-worker.js +1 -1
  13. package/dist/rpc-handler.js +0 -4
  14. package/dist/transports/http-transport.js +1 -0
  15. package/package.json +1 -1
  16. package/palmier-server/pwa/src/App.css +191 -33
  17. package/palmier-server/pwa/src/App.tsx +2 -0
  18. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +288 -0
  19. package/palmier-server/pwa/src/components/HostMenu.tsx +15 -312
  20. package/palmier-server/pwa/src/components/SessionComposer.tsx +11 -2
  21. package/palmier-server/pwa/src/components/SessionsView.tsx +3 -1
  22. package/palmier-server/pwa/src/components/TaskCard.tsx +1 -1
  23. package/palmier-server/pwa/src/components/TaskForm.tsx +126 -74
  24. package/palmier-server/pwa/src/components/TasksView.tsx +3 -1
  25. package/palmier-server/pwa/src/native/Device.ts +0 -2
  26. package/palmier-server/pwa/src/pages/Dashboard.tsx +2 -0
  27. package/palmier-server/pwa/src/pages/PairHost.tsx +3 -1
  28. package/palmier-server/pwa/src/pages/PairSetup.tsx +70 -0
  29. package/src/commands/pair.ts +2 -0
  30. package/src/commands/serve.ts +0 -3
  31. package/src/platform/index.ts +4 -3
  32. package/src/platform/macos.ts +310 -0
  33. package/src/rpc-handler.ts +0 -5
  34. package/src/transports/http-transport.ts +1 -0
  35. package/test/macos-plist.test.ts +112 -0
  36. package/dist/app-registry.d.ts +0 -10
  37. package/dist/app-registry.js +0 -44
  38. package/dist/pwa/assets/index-SYs3mcdJ.js +0 -120
  39. package/src/app-registry.ts +0 -52
@@ -5,6 +5,7 @@ import type { AgentInfo } from "../types";
5
5
 
6
6
  interface SessionComposerProps {
7
7
  agents: AgentInfo[];
8
+ hostPlatform?: string;
8
9
  onStarted(taskId: string, runId?: string): void;
9
10
  }
10
11
 
@@ -15,7 +16,7 @@ function pickDefaultAgent(agents: AgentInfo[]): string {
15
16
  return agents[0]?.key ?? "";
16
17
  }
17
18
 
18
- export default function SessionComposer({ agents, onStarted }: SessionComposerProps) {
19
+ export default function SessionComposer({ agents, hostPlatform, onStarted }: SessionComposerProps) {
19
20
  const { request } = useHostConnection();
20
21
  const [prompt, setPrompt] = useState("");
21
22
  const [agent, setAgent] = useState(() => pickDefaultAgent(agents));
@@ -62,7 +63,15 @@ export default function SessionComposer({ agents, onStarted }: SessionComposerPr
62
63
  try {
63
64
  const result = await request<{ task_id?: string; run_id?: string; error?: string }>(
64
65
  "task.run_oneoff",
65
- { user_prompt: prompt, agent, yolo_mode: yoloMode },
66
+ {
67
+ user_prompt: prompt,
68
+ agent,
69
+ yolo_mode: yoloMode,
70
+ // Direct runs on Windows need a visible session so interactive tools
71
+ // (browsers, GUI apps) can attach; background task-scheduler runs
72
+ // would otherwise land in session 0 with no display.
73
+ ...(hostPlatform === "win32" ? { foreground_mode: true } : {}),
74
+ },
66
75
  );
67
76
  if (result.error) {
68
77
  setError(result.error);
@@ -14,13 +14,14 @@ interface SessionsViewProps {
14
14
  request<T = unknown>(method: string, params?: unknown, opts?: { timeout?: number }): Promise<T>;
15
15
  subscribeEvents(hostId: string, callback: (msg: { subject: string; data: Uint8Array }) => void): () => void;
16
16
  agents: AgentInfo[];
17
+ hostPlatform?: string;
17
18
  filterTaskId?: string | null;
18
19
  onClearFilter?: () => void;
19
20
  }
20
21
 
21
22
  const PAGE_SIZE = 10;
22
23
 
23
- export default function SessionsView({ connected, hostId, request, subscribeEvents, agents, filterTaskId, onClearFilter }: SessionsViewProps) {
24
+ export default function SessionsView({ connected, hostId, request, subscribeEvents, agents, hostPlatform, filterTaskId, onClearFilter }: SessionsViewProps) {
24
25
  const [entries, setEntries] = useState<HistoryEntry[]>([]);
25
26
  const [total, setTotal] = useState(0);
26
27
  const [loading, setLoading] = useState(false);
@@ -175,6 +176,7 @@ export default function SessionsView({ connected, hostId, request, subscribeEven
175
176
  const composer = !filterTaskId && (
176
177
  <SessionComposer
177
178
  agents={agents}
179
+ hostPlatform={hostPlatform}
178
180
  onStarted={(taskId, runId) => {
179
181
  if (runId) navigate(`/runs/${encodeURIComponent(taskId)}/${encodeURIComponent(runId)}`);
180
182
  else navigate(`/runs/${encodeURIComponent(taskId)}`);
@@ -133,7 +133,7 @@ export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun
133
133
  values: string[] | undefined,
134
134
  ): string {
135
135
  if (!scheduleType) return "";
136
- if (scheduleType === "on_new_notification") return "On new push notification";
136
+ if (scheduleType === "on_new_notification") return "On new notification";
137
137
  if (scheduleType === "on_new_sms") return "On new SMS";
138
138
  if (!values || values.length === 0) return "";
139
139
  if (values.length === 1) return formatSingleValue(scheduleType, values[0]);
@@ -96,12 +96,13 @@ interface TaskFormProps {
96
96
  initial?: Task;
97
97
  agents: AgentInfo[];
98
98
  hostPlatform?: string;
99
+ isNotificationListener: boolean;
99
100
  onSaved(task: Task): void;
100
101
  onRun(taskId: string, runId?: string): void;
101
102
  onCancel(): void;
102
103
  }
103
104
 
104
- export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun, onCancel }: TaskFormProps) {
105
+ export default function TaskForm({ initial, agents, hostPlatform, isNotificationListener, onSaved, onRun, onCancel }: TaskFormProps) {
105
106
  const { request } = useHostConnection();
106
107
 
107
108
  const defaultAgent = () => {
@@ -154,46 +155,35 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
154
155
  return "";
155
156
  })();
156
157
  const [notificationApp, setNotificationApp] = useState<string>(initialNotificationApp);
157
- const [knownApps, setKnownApps] = useState<Array<{ packageName: string; appName: string; icon?: string }>>([]);
158
- const [appDropdownOpen, setAppDropdownOpen] = useState(false);
159
-
160
- // Merge native launcher enum (complete, native-only) with host registry
161
- // (apps already seen fills the gap on non-native clients). Dedup by
162
- // packageName; free-form typing still works if neither knows the package.
158
+ const [knownApps, setKnownApps] = useState<Array<{ packageName: string; appName: string }>>([]);
159
+ const [knownAppsLoading, setKnownAppsLoading] = useState(false);
160
+ const [appFilterOpen, setAppFilterOpen] = useState(false);
161
+ const [appSearch, setAppSearch] = useState("");
162
+ const closeAppFilter = useCallback(() => setAppFilterOpen(false), []);
163
+ useBackClose(appFilterOpen, closeAppFilter);
164
+
165
+ // Only the notification-listening device can enumerate installed apps. On any
166
+ // other client we leave the list empty and fall back to a plain packageName
167
+ // input — the app registry we used to maintain on the host was inconsistent
168
+ // across devices, so we no longer cache it.
163
169
  useEffect(() => {
164
170
  if (scheduleMode !== "on_new_notification") return;
171
+ if (!isNotificationListener || !Capacitor.isNativePlatform() || !Device) return;
165
172
  let cancelled = false;
166
-
167
- const merged = new Map<string, { packageName: string; appName: string; icon?: string }>();
168
- const PALMIER_PACKAGE = "com.palmier.app";
169
- const flush = () => { if (!cancelled) setKnownApps(Array.from(merged.values()).sort((a, b) => (a.appName || a.packageName).localeCompare(b.appName || b.packageName))); };
170
-
171
- if (Capacitor.isNativePlatform() && Device) {
172
- Device.getInstalledApps()
173
- .then(({ apps }) => {
174
- if (cancelled) return;
175
- for (const a of apps) {
176
- if (a.packageName === PALMIER_PACKAGE) continue;
177
- merged.set(a.packageName, { packageName: a.packageName, appName: a.appName, icon: a.icon });
178
- }
179
- flush();
180
- })
181
- .catch(() => {});
182
- }
183
-
184
- request<{ apps?: Array<{ packageName: string; appName: string }> }>("device.notifications.apps")
185
- .then((res) => {
186
- if (cancelled || !res.apps) return;
187
- for (const a of res.apps) {
188
- if (a.packageName === PALMIER_PACKAGE) continue;
189
- if (!merged.has(a.packageName)) merged.set(a.packageName, a);
190
- }
191
- flush();
173
+ setKnownAppsLoading(true);
174
+ Device.getInstalledApps()
175
+ .then(({ apps }) => {
176
+ if (cancelled) return;
177
+ const PALMIER_PACKAGE = "com.palmier.app";
178
+ const list = apps
179
+ .filter((a) => a.packageName !== PALMIER_PACKAGE)
180
+ .sort((a, b) => (a.appName || a.packageName).localeCompare(b.appName || b.packageName));
181
+ setKnownApps(list);
192
182
  })
193
- .catch(() => {});
194
-
183
+ .catch(() => {})
184
+ .finally(() => { if (!cancelled) setKnownAppsLoading(false); });
195
185
  return () => { cancelled = true; };
196
- }, [scheduleMode]);
186
+ }, [scheduleMode, isNotificationListener]);
197
187
 
198
188
  // Sender filter for on_new_sms tasks. Empty string = any sender; non-empty =
199
189
  // whitelist a single sender (stored as a single-entry schedule_values array
@@ -444,7 +434,7 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
444
434
  <option value="daily">Daily</option>
445
435
  <option value="weekly">Weekly</option>
446
436
  <option value="monthly">Monthly</option>
447
- <option value="on_new_notification">On New Push Notification</option>
437
+ <option value="on_new_notification">On New Notification</option>
448
438
  <option value="on_new_sms">On New SMS</option>
449
439
  <option value="command">Command-triggered</option>
450
440
  </select>
@@ -453,57 +443,53 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
453
443
  <div className="schedule-reactive">
454
444
  <p className="command-help-text">
455
445
  {scheduleMode === "on_new_notification"
456
- ? "Runs each time a new notification arrives on the paired Android device."
446
+ ? "Runs each time a new push notification arrives on the paired Android device."
457
447
  : "Runs each time a new SMS arrives on the paired Android device."}
458
448
  {" "}The triggering payload is spliced into your task prompt — reference it as &ldquo;the new {scheduleMode === "on_new_notification" ? "notification" : "SMS"}&rdquo;.
459
449
  </p>
460
450
  {scheduleMode === "on_new_notification" && (() => {
461
- const q = notificationApp.trim().toLowerCase();
462
- const filtered = q
463
- ? knownApps.filter((a) => a.packageName.toLowerCase().includes(q) || a.appName.toLowerCase().includes(q))
464
- : knownApps;
451
+ const selected = knownApps.find((a) => a.packageName === notificationApp);
452
+ const selectedLabel = selected?.appName || notificationApp;
465
453
  return (
466
454
  <>
467
- <div className="app-combobox">
455
+ {isNotificationListener ? (
456
+ notificationApp.trim() ? (
457
+ <div className="app-filter-selected">
458
+ <span className="app-filter-selected-name">{selectedLabel}</span>
459
+ {selected?.appName && <span className="app-filter-selected-pkg">{selected.packageName}</span>}
460
+ <button
461
+ type="button"
462
+ className="app-filter-selected-clear"
463
+ onClick={() => setNotificationApp("")}
464
+ aria-label="Clear app filter"
465
+ disabled={saving}
466
+ >
467
+
468
+ </button>
469
+ </div>
470
+ ) : (
471
+ <button
472
+ type="button"
473
+ className="btn btn-link app-filter-trigger"
474
+ onClick={() => { setAppSearch(""); setAppFilterOpen(true); }}
475
+ disabled={saving}
476
+ >
477
+ Select app
478
+ </button>
479
+ )
480
+ ) : (
468
481
  <input
469
482
  className="form-input"
470
483
  type="text"
471
484
  value={notificationApp}
472
- onChange={(e) => { setNotificationApp(e.target.value); setAppDropdownOpen(true); }}
473
- onFocus={() => setAppDropdownOpen(true)}
474
- onBlur={() => setAppDropdownOpen(false)}
475
- placeholder={Capacitor.isNativePlatform() ? "App (optional)" : "App (optional, e.g. com.google.android.gm)"}
485
+ onChange={(e) => setNotificationApp(e.target.value)}
486
+ placeholder="App (optional), e.g. com.google.android.gm"
476
487
  disabled={saving}
477
488
  />
478
- {appDropdownOpen && filtered.length > 0 && (
479
- <ul className="app-combobox-list">
480
- {filtered.map((a) => (
481
- <li
482
- key={a.packageName}
483
- className="app-combobox-row"
484
- // onMouseDown instead of onClick so the input's blur doesn't close
485
- // the dropdown before the selection registers.
486
- onMouseDown={(e) => {
487
- e.preventDefault();
488
- setNotificationApp(a.packageName);
489
- setAppDropdownOpen(false);
490
- }}
491
- >
492
- {a.icon
493
- ? <img src={a.icon} alt="" className="app-combobox-icon" />
494
- : <div className="app-combobox-icon app-combobox-icon-placeholder" />}
495
- <div className="app-combobox-labels">
496
- <div className="app-combobox-name">{a.appName || a.packageName}</div>
497
- {a.appName && <div className="app-combobox-pkg">{a.packageName}</div>}
498
- </div>
499
- </li>
500
- ))}
501
- </ul>
502
- )}
503
- </div>
489
+ )}
504
490
  <p className="command-help-text app-filter-help">
505
491
  {notificationApp.trim()
506
- ? `Only notifications from ${notificationApp.trim()} will trigger this task.`
492
+ ? `Only notifications from ${selectedLabel} will trigger this task.`
507
493
  : "Every notification from your device triggers this task."}
508
494
  </p>
509
495
  </>
@@ -699,6 +685,72 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
699
685
 
700
686
  </>)}
701
687
  </div>
688
+ {appFilterOpen && (() => {
689
+ const q = appSearch.trim().toLowerCase();
690
+ const filtered = q
691
+ ? knownApps.filter((a) => a.packageName.toLowerCase().includes(q) || a.appName.toLowerCase().includes(q))
692
+ : knownApps;
693
+ return (
694
+ <div className="app-filter-overlay" onClick={closeAppFilter}>
695
+ <div className="app-filter-dialog" onClick={(e) => e.stopPropagation()}>
696
+ <div className="app-filter-header">
697
+ <h2>Select app</h2>
698
+ <button className="app-filter-close" onClick={closeAppFilter} aria-label="Close">
699
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
700
+ <path d="M4 4L12 12M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
701
+ </svg>
702
+ </button>
703
+ </div>
704
+ <input
705
+ className="form-input app-filter-search"
706
+ type="text"
707
+ value={appSearch}
708
+ onChange={(e) => setAppSearch(e.target.value)}
709
+ onKeyDown={(e) => {
710
+ if (e.key === "Enter" && appSearch.trim()) {
711
+ setNotificationApp(appSearch.trim());
712
+ closeAppFilter();
713
+ }
714
+ }}
715
+ placeholder="Search or type a package name"
716
+ autoFocus
717
+ />
718
+ <ul className="app-filter-list">
719
+ {knownAppsLoading && knownApps.length === 0
720
+ ? Array.from({ length: 6 }).map((_, i) => (
721
+ <li key={`sk-${i}`} className="app-filter-row app-filter-skeleton">
722
+ <div className="app-filter-skeleton-bar" />
723
+ </li>
724
+ ))
725
+ : filtered.length === 0 && !appSearch.trim()
726
+ ? <li className="app-filter-empty">No apps</li>
727
+ : filtered.map((a) => (
728
+ <li
729
+ key={a.packageName}
730
+ className="app-filter-row"
731
+ onClick={() => { setNotificationApp(a.packageName); closeAppFilter(); }}
732
+ >
733
+ <div className="app-filter-row-labels">
734
+ <div className="app-filter-row-name">{a.appName || a.packageName}</div>
735
+ {a.appName && <div className="app-filter-row-pkg">{a.packageName}</div>}
736
+ </div>
737
+ </li>
738
+ ))}
739
+ {appSearch.trim() && (
740
+ <li
741
+ className="app-filter-row"
742
+ onClick={() => { setNotificationApp(appSearch.trim()); closeAppFilter(); }}
743
+ >
744
+ <div className="app-filter-row-labels">
745
+ <div className="app-filter-row-name">{appSearch.trim()}</div>
746
+ </div>
747
+ </li>
748
+ )}
749
+ </ul>
750
+ </div>
751
+ </div>
752
+ );
753
+ })()}
702
754
  </div>
703
755
  );
704
756
  }
@@ -12,10 +12,11 @@ interface TasksViewProps {
12
12
  subscribeEvents(hostId: string, callback: (msg: { subject: string; data: Uint8Array }) => void): () => void;
13
13
  agents: AgentInfo[];
14
14
  hostPlatform?: string;
15
+ isNotificationListener: boolean;
15
16
  onViewRun(taskId: string, runId?: string): void;
16
17
  }
17
18
 
18
- export default function TasksView({ connected, hostId, request, subscribeEvents, agents, hostPlatform, onViewRun }: TasksViewProps) {
19
+ export default function TasksView({ connected, hostId, request, subscribeEvents, agents, hostPlatform, isNotificationListener, onViewRun }: TasksViewProps) {
19
20
  const [tasks, setTasks] = useState<Task[]>([]);
20
21
  const [loadingTasks, setLoadingTasks] = useState(false);
21
22
  const [taskError, setTaskError] = useState<string | null>(null);
@@ -167,6 +168,7 @@ export default function TasksView({ connected, hostId, request, subscribeEvents,
167
168
  initial={editingTask}
168
169
  agents={agents}
169
170
  hostPlatform={hostPlatform}
171
+ isNotificationListener={isNotificationListener}
170
172
  onSaved={handleTaskSaved}
171
173
  onRun={onViewRun}
172
174
  onCancel={closeForm}
@@ -28,8 +28,6 @@ export interface DeepLinkEvent {
28
28
  export interface InstalledApp {
29
29
  packageName: string;
30
30
  appName: string;
31
- /** data:image/png;base64 URL, 96x96. May be absent if the icon couldn't be rendered. */
32
- icon?: string;
33
31
  }
34
32
 
35
33
  interface DevicePlugin {
@@ -348,6 +348,7 @@ export default function Dashboard() {
348
348
  subscribeEvents={subscribeEvents}
349
349
  agents={agents}
350
350
  hostPlatform={hostPlatform}
351
+ isNotificationListener={!!activeClientToken && capabilityTokens["notifications"] === activeClientToken}
351
352
  onViewRun={handleViewRun}
352
353
  />
353
354
  )}
@@ -367,6 +368,7 @@ export default function Dashboard() {
367
368
  request={request}
368
369
  subscribeEvents={subscribeEvents}
369
370
  agents={agents}
371
+ hostPlatform={hostPlatform}
370
372
  filterTaskId={runsFilterTaskId}
371
373
  onClearFilter={() => { if (confirmLeaveDraft()) navigate("/"); }}
372
374
  />
@@ -12,6 +12,7 @@ interface PairResponse {
12
12
  hostId: string;
13
13
  clientToken: string;
14
14
  directUrl?: string;
15
+ hostName?: string;
15
16
  }
16
17
 
17
18
  /** LAN mode: PWA is served by palmier serve (marker injected into HTML). */
@@ -82,6 +83,7 @@ export default function PairHost() {
82
83
  hostId: response.hostId,
83
84
  clientToken: response.clientToken,
84
85
  directUrl: isLanMode ? window.location.origin : undefined,
86
+ ...(response.hostName ? { name: response.hostName } : {}),
85
87
  };
86
88
 
87
89
  addPairedHost(host);
@@ -106,7 +108,7 @@ export default function PairHost() {
106
108
  }
107
109
  }
108
110
 
109
- navigate("/");
111
+ navigate(Capacitor.isNativePlatform() ? "/pair/setup" : "/");
110
112
  } catch (err) {
111
113
  const message = err instanceof Error ? err.message : String(err);
112
114
  if (message.includes("timeout") || message.includes("TIMEOUT") || message.includes("503") || message.toLowerCase().includes("no responders")) {
@@ -0,0 +1,70 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import { useHostConnection } from "../contexts/HostConnectionContext";
4
+ import { useHostStore } from "../contexts/HostStoreContext";
5
+ import CapabilityToggles from "../components/CapabilityToggles";
6
+
7
+ interface HostInfoResponse {
8
+ capability_tokens?: Record<string, string | null>;
9
+ }
10
+
11
+ export default function PairSetup() {
12
+ const navigate = useNavigate();
13
+ const { connected, request } = useHostConnection();
14
+ const { getActiveHost } = useHostStore();
15
+ const activeHost = getActiveHost();
16
+ const activeClientToken = activeHost?.clientToken ?? null;
17
+
18
+ const [capabilityTokens, setCapabilityTokens] = useState<Record<string, string | null>>({});
19
+ const [loaded, setLoaded] = useState(false);
20
+
21
+ // If the user lands here without an active host (direct URL, refresh), bounce
22
+ // back to the dashboard — setup only makes sense right after pairing.
23
+ useEffect(() => {
24
+ if (!activeHost) navigate("/", { replace: true });
25
+ }, [activeHost, navigate]);
26
+
27
+ useEffect(() => {
28
+ if (!connected || !activeHost) return;
29
+ let cancelled = false;
30
+ request<HostInfoResponse>("host.info")
31
+ .then((res) => {
32
+ if (cancelled) return;
33
+ setCapabilityTokens(res.capability_tokens ?? {});
34
+ setLoaded(true);
35
+ })
36
+ .catch(() => { if (!cancelled) setLoaded(true); });
37
+ return () => { cancelled = true; };
38
+ }, [connected, activeHost, request]);
39
+
40
+ return (
41
+ <div className="pair-setup">
42
+ <div className="pair-setup-inner">
43
+ <h1 className="pair-setup-title">What capabilities of this device do you want your host computer to have?</h1>
44
+ <p className="pair-setup-description">
45
+ You can change these later from the menu.
46
+ </p>
47
+
48
+ {!loaded ? (
49
+ <div className="pair-setup-loading">Connecting to host…</div>
50
+ ) : (
51
+ <CapabilityToggles
52
+ capabilityTokens={capabilityTokens}
53
+ activeClientToken={activeClientToken}
54
+ request={request}
55
+ onCapabilityTokensChange={setCapabilityTokens}
56
+ />
57
+ )}
58
+
59
+ <div className="pair-setup-actions">
60
+ <button
61
+ className="btn btn-primary btn-full"
62
+ onClick={() => navigate("/", { replace: true })}
63
+ >
64
+ Finish
65
+ </button>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ );
70
+ }
@@ -1,4 +1,5 @@
1
1
  import * as http from "node:http";
2
+ import * as os from "node:os";
2
3
  import { StringCodec } from "nats";
3
4
  import { loadConfig } from "../config.js";
4
5
  import { connectNats } from "../nats-client.js";
@@ -21,6 +22,7 @@ function buildPairResponse(config: HostConfig, label?: string) {
21
22
  return {
22
23
  hostId: config.hostId,
23
24
  clientToken: client.token,
25
+ hostName: os.hostname(),
24
26
  };
25
27
  }
26
28
 
@@ -16,7 +16,6 @@ import { StringCodec, type NatsConnection } from "nats";
16
16
  import { addNotification } from "../notification-store.js";
17
17
  import { addSmsMessage } from "../sms-store.js";
18
18
  import { enqueueEvent } from "../event-queues.js";
19
- import { recordApp } from "../app-registry.js";
20
19
 
21
20
  const POLL_INTERVAL_MS = 30_000;
22
21
  const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
@@ -159,9 +158,7 @@ export async function serveCommand(): Promise<void> {
159
158
  let parsed: unknown;
160
159
  try {
161
160
  parsed = JSON.parse(raw);
162
- const data = parsed as { packageName?: string; appName?: string };
163
161
  addNotification({ ...(parsed as object), receivedAt: Date.now() } as Parameters<typeof addNotification>[0]);
164
- if (data.packageName && data.appName) recordApp(data.packageName, data.appName);
165
162
  } catch (err) {
166
163
  console.error("[nats] Failed to parse device notification:", err);
167
164
  }
@@ -1,6 +1,7 @@
1
1
  import type { PlatformService } from "./platform.js";
2
2
  import { LinuxPlatform } from "./linux.js";
3
3
  import { WindowsPlatform } from "./windows.js";
4
+ import { MacOsPlatform } from "./macos.js";
4
5
 
5
6
  /** Windows needs an explicit shell for execSync to resolve .cmd shims. */
6
7
  export const SHELL: string | undefined = process.platform === "win32" ? "cmd.exe" : undefined;
@@ -9,9 +10,9 @@ let _instance: PlatformService | undefined;
9
10
 
10
11
  export function getPlatform(): PlatformService {
11
12
  if (!_instance) {
12
- _instance = process.platform === "win32"
13
- ? new WindowsPlatform()
14
- : new LinuxPlatform();
13
+ if (process.platform === "win32") _instance = new WindowsPlatform();
14
+ else if (process.platform === "darwin") _instance = new MacOsPlatform();
15
+ else _instance = new LinuxPlatform();
15
16
  }
16
17
  return _instance;
17
18
  }