palmier 0.9.6 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -13
- package/dist/agents/agent.d.ts +0 -1
- package/dist/agents/agent.js +0 -1
- package/dist/agents/aider.d.ts +0 -1
- package/dist/agents/aider.js +0 -1
- package/dist/agents/claude.d.ts +0 -1
- package/dist/agents/claude.js +0 -1
- package/dist/agents/cline.d.ts +0 -1
- package/dist/agents/cline.js +0 -1
- package/dist/agents/codex.d.ts +0 -1
- package/dist/agents/codex.js +0 -1
- package/dist/agents/copilot.d.ts +0 -1
- package/dist/agents/copilot.js +0 -1
- package/dist/agents/cursor.d.ts +0 -1
- package/dist/agents/cursor.js +0 -1
- package/dist/agents/deepagents.d.ts +0 -1
- package/dist/agents/deepagents.js +0 -1
- package/dist/agents/droid.d.ts +0 -1
- package/dist/agents/droid.js +0 -1
- package/dist/agents/gemini.d.ts +0 -1
- package/dist/agents/gemini.js +0 -1
- package/dist/agents/goose.d.ts +0 -1
- package/dist/agents/goose.js +0 -1
- package/dist/agents/hermes.d.ts +0 -1
- package/dist/agents/hermes.js +0 -1
- package/dist/agents/kimi.d.ts +0 -1
- package/dist/agents/kimi.js +0 -1
- package/dist/agents/kiro.d.ts +0 -1
- package/dist/agents/kiro.js +0 -1
- package/dist/agents/openclaw.d.ts +0 -1
- package/dist/agents/openclaw.js +0 -1
- package/dist/agents/opencode.d.ts +0 -1
- package/dist/agents/opencode.js +0 -1
- package/dist/agents/qoder.d.ts +0 -1
- package/dist/agents/qoder.js +0 -1
- package/dist/agents/qwen.d.ts +0 -1
- package/dist/agents/qwen.js +0 -1
- package/dist/agents/shared-prompt.d.ts +0 -1
- package/dist/agents/shared-prompt.js +0 -1
- package/dist/client-store.d.ts +0 -1
- package/dist/client-store.js +0 -1
- package/dist/commands/clients.d.ts +0 -1
- package/dist/commands/clients.js +0 -1
- package/dist/commands/info.d.ts +0 -1
- package/dist/commands/info.js +0 -1
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +1 -2
- package/dist/commands/pair.d.ts +0 -1
- package/dist/commands/pair.js +0 -1
- package/dist/commands/restart.d.ts +0 -1
- package/dist/commands/restart.js +0 -1
- package/dist/commands/run.d.ts +0 -1
- package/dist/commands/run.js +19 -3
- package/dist/commands/serve.d.ts +0 -1
- package/dist/commands/serve.js +0 -1
- package/dist/commands/uninstall.d.ts +0 -1
- package/dist/commands/uninstall.js +0 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/event-queues.d.ts +0 -1
- package/dist/event-queues.js +0 -1
- package/dist/events.d.ts +0 -1
- package/dist/events.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/linked-device.d.ts +0 -1
- package/dist/linked-device.js +0 -1
- package/dist/mcp-handler.d.ts +0 -1
- package/dist/mcp-handler.js +0 -1
- package/dist/mcp-tools.d.ts +0 -1
- package/dist/mcp-tools.js +0 -1
- package/dist/nats-client.d.ts +0 -1
- package/dist/nats-client.js +0 -1
- package/dist/network.d.ts +0 -1
- package/dist/network.js +0 -1
- package/dist/notification-store.d.ts +0 -1
- package/dist/notification-store.js +0 -1
- package/dist/pending-requests.d.ts +0 -1
- package/dist/pending-requests.js +0 -1
- package/dist/platform/index.d.ts +0 -1
- package/dist/platform/index.js +0 -1
- package/dist/platform/linux.d.ts +0 -1
- package/dist/platform/linux.js +0 -1
- package/dist/platform/macos.d.ts +0 -1
- package/dist/platform/macos.js +0 -1
- package/dist/platform/platform.d.ts +0 -1
- package/dist/platform/platform.js +0 -1
- package/dist/platform/windows.d.ts +0 -1
- package/dist/platform/windows.js +0 -1
- package/dist/pwa/assets/{index-MLEFUP3r.js → index-DWvRAUiy.js} +31 -31
- package/dist/pwa/assets/{web-B1sKCc7e.js → web-C4iZbqTC.js} +1 -1
- package/dist/pwa/assets/{web-ETD-8ZHd.js → web-CBFqJGX6.js} +1 -1
- package/dist/pwa/assets/{web-B4xEa6WO.js → web-DL4uXOpS.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/rpc-handler.d.ts +0 -1
- package/dist/rpc-handler.js +0 -1
- package/dist/sms-store.d.ts +0 -1
- package/dist/sms-store.js +0 -1
- package/dist/spawn-command.d.ts +0 -1
- package/dist/spawn-command.js +0 -1
- package/dist/task.d.ts +0 -1
- package/dist/task.js +0 -1
- package/dist/transports/http-transport.d.ts +0 -1
- package/dist/transports/http-transport.js +0 -1
- package/dist/transports/nats-transport.d.ts +0 -1
- package/dist/transports/nats-transport.js +0 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/dist/update-checker.d.ts +0 -1
- package/dist/update-checker.js +0 -1
- package/package.json +11 -1
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -37
- package/CLAUDE.md +0 -22
- package/dist/pwa/apple-touch-icon.png +0 -0
- package/dist/pwa/manifest.webmanifest +0 -1
- package/dist/pwa/pwa-192x192.png +0 -0
- package/dist/pwa/pwa-512x512.png +0 -0
- package/dist/pwa/registerSW.js +0 -1
- package/dist/pwa/service-worker.js +0 -2
- package/palmier-server/.github/workflows/ci.yml +0 -21
- package/palmier-server/.github/workflows/deploy.yml +0 -38
- package/palmier-server/CLAUDE.md +0 -17
- package/palmier-server/PRODUCTION.md +0 -358
- package/palmier-server/README.md +0 -231
- package/palmier-server/nats.conf +0 -19
- package/palmier-server/package.json +0 -15
- package/palmier-server/pnpm-lock.yaml +0 -7639
- package/palmier-server/pnpm-workspace.yaml +0 -3
- package/palmier-server/pwa/index.html +0 -16
- package/palmier-server/pwa/logo/logo_20260421.png +0 -0
- package/palmier-server/pwa/package.json +0 -34
- package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
- package/palmier-server/pwa/public/favicon.ico +0 -0
- package/palmier-server/pwa/public/pwa-192x192.png +0 -0
- package/palmier-server/pwa/public/pwa-512x512.png +0 -0
- package/palmier-server/pwa/src/App.css +0 -3012
- package/palmier-server/pwa/src/App.tsx +0 -59
- package/palmier-server/pwa/src/agentLabels.ts +0 -11
- package/palmier-server/pwa/src/api.ts +0 -67
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
- package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -113
- package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
- package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
- package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
- package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
- package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
- package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
- package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
- package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
- package/palmier-server/pwa/src/components/TaskForm.tsx +0 -766
- package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
- package/palmier-server/pwa/src/constants.ts +0 -2
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
- package/palmier-server/pwa/src/draftGuard.ts +0 -24
- package/palmier-server/pwa/src/formatTime.ts +0 -44
- package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
- package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
- package/palmier-server/pwa/src/main.tsx +0 -14
- package/palmier-server/pwa/src/native/Device.ts +0 -49
- package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
- package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
- package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
- package/palmier-server/pwa/src/service-worker.ts +0 -142
- package/palmier-server/pwa/src/types.ts +0 -75
- package/palmier-server/pwa/src/vite-env.d.ts +0 -11
- package/palmier-server/pwa/tsconfig.json +0 -21
- package/palmier-server/pwa/tsconfig.node.json +0 -19
- package/palmier-server/pwa/vite.config.ts +0 -47
- package/palmier-server/server/.env.example +0 -20
- package/palmier-server/server/package.json +0 -36
- package/palmier-server/server/src/db.ts +0 -44
- package/palmier-server/server/src/fcm.ts +0 -74
- package/palmier-server/server/src/index.ts +0 -688
- package/palmier-server/server/src/nats-jwt.ts +0 -299
- package/palmier-server/server/src/nats-setup.ts +0 -48
- package/palmier-server/server/src/nats.ts +0 -33
- package/palmier-server/server/src/notify.ts +0 -34
- package/palmier-server/server/src/push.ts +0 -68
- package/palmier-server/server/src/routes/device.ts +0 -224
- package/palmier-server/server/src/routes/fcm.ts +0 -64
- package/palmier-server/server/src/routes/hosts.ts +0 -56
- package/palmier-server/server/src/routes/push.ts +0 -101
- package/palmier-server/server/tsconfig.json +0 -20
- package/palmier-server/spec.md +0 -533
- package/src/agents/agent-instructions.md +0 -28
- package/src/agents/agent.ts +0 -114
- package/src/agents/aider.ts +0 -35
- package/src/agents/claude.ts +0 -39
- package/src/agents/cline.ts +0 -35
- package/src/agents/codex.ts +0 -40
- package/src/agents/copilot.ts +0 -37
- package/src/agents/cursor.ts +0 -36
- package/src/agents/deepagents.ts +0 -36
- package/src/agents/droid.ts +0 -35
- package/src/agents/gemini.ts +0 -43
- package/src/agents/goose.ts +0 -33
- package/src/agents/hermes.ts +0 -36
- package/src/agents/kimi.ts +0 -35
- package/src/agents/kiro.ts +0 -36
- package/src/agents/openclaw.ts +0 -29
- package/src/agents/opencode.ts +0 -36
- package/src/agents/qoder.ts +0 -36
- package/src/agents/qwen.ts +0 -32
- package/src/agents/shared-prompt.ts +0 -30
- package/src/client-store.ts +0 -68
- package/src/commands/clients.ts +0 -29
- package/src/commands/info.ts +0 -29
- package/src/commands/init.ts +0 -165
- package/src/commands/pair.ts +0 -137
- package/src/commands/restart.ts +0 -6
- package/src/commands/run.ts +0 -608
- package/src/commands/serve.ts +0 -211
- package/src/commands/uninstall.ts +0 -9
- package/src/config.ts +0 -36
- package/src/cross-spawn.d.ts +0 -5
- package/src/event-queues.ts +0 -41
- package/src/events.ts +0 -29
- package/src/index.ts +0 -111
- package/src/linked-device.ts +0 -52
- package/src/mcp-handler.ts +0 -200
- package/src/mcp-tools.ts +0 -839
- package/src/nats-client.ts +0 -19
- package/src/network.ts +0 -96
- package/src/notification-store.ts +0 -30
- package/src/pending-requests.ts +0 -73
- package/src/platform/index.ts +0 -20
- package/src/platform/linux.ts +0 -296
- package/src/platform/macos.ts +0 -329
- package/src/platform/platform.ts +0 -31
- package/src/platform/windows.ts +0 -299
- package/src/rpc-handler.ts +0 -691
- package/src/sms-store.ts +0 -28
- package/src/spawn-command.ts +0 -123
- package/src/task.ts +0 -343
- package/src/transports/http-transport.ts +0 -478
- package/src/transports/nats-transport.ts +0 -76
- package/src/types.ts +0 -89
- package/src/update-checker.ts +0 -40
- package/test/agent-instructions.test.ts +0 -209
- package/test/agent-output-parsing.test.ts +0 -74
- package/test/linux-cron.test.ts +0 -41
- package/test/macos-plist.test.ts +0 -112
- package/test/notification-store.test.ts +0 -57
- package/test/pairing.test.ts +0 -35
- package/test/result-state.test.ts +0 -110
- package/test/task-parsing.test.ts +0 -82
- package/test/taskrun-messages.test.ts +0 -224
- package/test/tsconfig.json +0 -9
- package/test/windows-xml.test.ts +0 -89
- package/tsconfig.json +0 -19
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { useNavigate } from "react-router-dom";
|
|
3
|
-
import { connect, jwtAuthenticator, StringCodec } from "nats.ws";
|
|
4
|
-
import { Capacitor } from "@capacitor/core";
|
|
5
|
-
import { Preferences } from "@capacitor/preferences";
|
|
6
|
-
import { useHostStore } from "../contexts/HostStoreContext";
|
|
7
|
-
import { Device } from "../native/Device";
|
|
8
|
-
import { SERVER_URL } from "../api";
|
|
9
|
-
import type { PairedHost } from "../types";
|
|
10
|
-
|
|
11
|
-
interface PairResponse {
|
|
12
|
-
hostId: string;
|
|
13
|
-
clientToken: string;
|
|
14
|
-
hostName?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Loopback (Local) mode: PWA is served by palmier serve on localhost. */
|
|
18
|
-
const isLoopback = !!(window as any).__PALMIER_SERVE__
|
|
19
|
-
&& (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
|
|
20
|
-
|
|
21
|
-
const isNative = Capacitor.isNativePlatform();
|
|
22
|
-
|
|
23
|
-
export default function PairHost() {
|
|
24
|
-
const [code, setCode] = useState("");
|
|
25
|
-
const [makeLinked, setMakeLinked] = useState(true);
|
|
26
|
-
const [pairing, setPairing] = useState(false);
|
|
27
|
-
const [error, setError] = useState<string | null>(null);
|
|
28
|
-
const { addPairedHost } = useHostStore();
|
|
29
|
-
const navigate = useNavigate();
|
|
30
|
-
|
|
31
|
-
async function handlePair() {
|
|
32
|
-
const trimmedCode = code.trim().toUpperCase();
|
|
33
|
-
if (!trimmedCode) {
|
|
34
|
-
setError("Enter a pairing code.");
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
setPairing(true);
|
|
39
|
-
setError(null);
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
let response: PairResponse;
|
|
43
|
-
|
|
44
|
-
if (isLoopback) {
|
|
45
|
-
const res = await fetch("/pair", {
|
|
46
|
-
method: "POST",
|
|
47
|
-
headers: { "Content-Type": "application/json" },
|
|
48
|
-
body: JSON.stringify({ code: trimmedCode, label: navigator.userAgent }),
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (!res.ok) {
|
|
52
|
-
const body = await res.json().catch(() => ({ error: "Connection failed" })) as { error?: string };
|
|
53
|
-
throw new Error(body.error || `HTTP ${res.status}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
response = await res.json() as PairResponse;
|
|
57
|
-
} else {
|
|
58
|
-
const configRes = await fetch(`${SERVER_URL}/api/config`);
|
|
59
|
-
if (!configRes.ok) throw new Error("Failed to fetch server config");
|
|
60
|
-
const config = await configRes.json() as { natsWsUrl: string; natsJwt: string; natsNkeySeed: string };
|
|
61
|
-
if (!config.natsWsUrl) throw new Error("Server has no NATS WebSocket URL configured");
|
|
62
|
-
|
|
63
|
-
const nc = await connect({
|
|
64
|
-
servers: config.natsWsUrl,
|
|
65
|
-
authenticator: jwtAuthenticator(
|
|
66
|
-
config.natsJwt,
|
|
67
|
-
new TextEncoder().encode(config.natsNkeySeed),
|
|
68
|
-
),
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const sc = StringCodec();
|
|
72
|
-
const subject = `pair.${trimmedCode}`;
|
|
73
|
-
const msg = await nc.request(
|
|
74
|
-
subject,
|
|
75
|
-
sc.encode(JSON.stringify({ label: navigator.userAgent })),
|
|
76
|
-
{ timeout: 10000 },
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
response = JSON.parse(sc.decode(msg.data)) as PairResponse;
|
|
80
|
-
await nc.close();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const host: PairedHost = {
|
|
84
|
-
hostId: response.hostId,
|
|
85
|
-
clientToken: response.clientToken,
|
|
86
|
-
directUrl: isLoopback ? window.location.origin : undefined,
|
|
87
|
-
...(response.hostName ? { name: response.hostName } : {}),
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
addPairedHost(host);
|
|
91
|
-
|
|
92
|
-
if (Capacitor.isNativePlatform() && Device) {
|
|
93
|
-
// Native receivers (SmsBroadcastReceiver, DeviceNotificationListenerService)
|
|
94
|
-
// read hostId to address relay messages.
|
|
95
|
-
await Preferences.set({ key: "hostId", value: response.hostId });
|
|
96
|
-
|
|
97
|
-
// Register this device's FCM token with the relay server so it can wake
|
|
98
|
-
// the device on the paired host's behalf. Moved here from native so the
|
|
99
|
-
// APK no longer needs to read hostId or trigger registration itself.
|
|
100
|
-
try {
|
|
101
|
-
const { token: fcmToken } = await Device.getFcmToken();
|
|
102
|
-
await fetch(`${SERVER_URL}/api/fcm/register`, {
|
|
103
|
-
method: "POST",
|
|
104
|
-
headers: { "Content-Type": "application/json" },
|
|
105
|
-
body: JSON.stringify({ hostId: response.hostId, fcmToken }),
|
|
106
|
-
});
|
|
107
|
-
} catch (err) {
|
|
108
|
-
console.warn("FCM token registration failed:", err);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const base = `/hosts/${encodeURIComponent(response.hostId)}`;
|
|
113
|
-
navigate(isNative && makeLinked ? `${base}/pair/setup` : base);
|
|
114
|
-
} catch (err) {
|
|
115
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
116
|
-
if (message.includes("timeout") || message.includes("TIMEOUT") || message.includes("503") || message.toLowerCase().includes("no responders")) {
|
|
117
|
-
setError("Code not found or expired. Check the code and try again.");
|
|
118
|
-
} else {
|
|
119
|
-
setError(message);
|
|
120
|
-
}
|
|
121
|
-
} finally {
|
|
122
|
-
setPairing(false);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<div className="pair-page">
|
|
128
|
-
<div className="pair-card">
|
|
129
|
-
<div className="pair-header">
|
|
130
|
-
<h1 className="pair-title">{isLoopback ? "Pair" : "Pair with Host"}</h1>
|
|
131
|
-
<p className="pair-subtitle">
|
|
132
|
-
{isLoopback
|
|
133
|
-
? "Enter the pairing code shown in your terminal."
|
|
134
|
-
: "Connect this device to a Palmier host"}
|
|
135
|
-
</p>
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
{!isLoopback && (
|
|
139
|
-
<div className="pair-instructions">
|
|
140
|
-
<div className="pair-instruction-block">
|
|
141
|
-
<h3 className="pair-instruction-heading">Setting up a new host?</h3>
|
|
142
|
-
<ol className="pair-steps">
|
|
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
|
-
<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>
|
|
149
|
-
</li>
|
|
150
|
-
<li>Run the setup wizard:
|
|
151
|
-
<code className="pair-command">palmier init</code>
|
|
152
|
-
</li>
|
|
153
|
-
<li>A pairing code will display automatically</li>
|
|
154
|
-
</ol>
|
|
155
|
-
</div>
|
|
156
|
-
<div className="pair-instruction-divider" />
|
|
157
|
-
<div className="pair-instruction-block">
|
|
158
|
-
<h3 className="pair-instruction-heading">Pairing an existing host?</h3>
|
|
159
|
-
<ol className="pair-steps">
|
|
160
|
-
<li>
|
|
161
|
-
Run <code>palmier pair</code> on the host machine
|
|
162
|
-
</li>
|
|
163
|
-
<li>Enter the 6-character code below</li>
|
|
164
|
-
</ol>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
168
|
-
|
|
169
|
-
<div className="pair-form">
|
|
170
|
-
<label className="form-label" htmlFor="pair-code">
|
|
171
|
-
Pairing code
|
|
172
|
-
<input
|
|
173
|
-
id="pair-code"
|
|
174
|
-
type="text"
|
|
175
|
-
maxLength={6}
|
|
176
|
-
value={code}
|
|
177
|
-
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
178
|
-
placeholder="A7K9M2"
|
|
179
|
-
className="form-input form-input-mono pair-code-input"
|
|
180
|
-
autoFocus
|
|
181
|
-
autoComplete="off"
|
|
182
|
-
disabled={pairing}
|
|
183
|
-
/>
|
|
184
|
-
</label>
|
|
185
|
-
|
|
186
|
-
{isNative && (
|
|
187
|
-
<label className="pair-checkbox">
|
|
188
|
-
<input
|
|
189
|
-
type="checkbox"
|
|
190
|
-
checked={makeLinked}
|
|
191
|
-
onChange={(e) => setMakeLinked(e.target.checked)}
|
|
192
|
-
disabled={pairing}
|
|
193
|
-
/>
|
|
194
|
-
<span className="pair-checkbox-text">
|
|
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>
|
|
201
|
-
</span>
|
|
202
|
-
</label>
|
|
203
|
-
)}
|
|
204
|
-
|
|
205
|
-
{error && <p className="pair-error">{error}</p>}
|
|
206
|
-
|
|
207
|
-
<button
|
|
208
|
-
className="btn btn-primary btn-full"
|
|
209
|
-
onClick={handlePair}
|
|
210
|
-
disabled={pairing || !code.trim()}
|
|
211
|
-
>
|
|
212
|
-
{pairing && <span className="btn-spinner" />}
|
|
213
|
-
{pairing ? "Pairing..." : "Pair"}
|
|
214
|
-
</button>
|
|
215
|
-
<button
|
|
216
|
-
className="btn btn-secondary btn-full"
|
|
217
|
-
onClick={() => navigate("/")}
|
|
218
|
-
disabled={pairing}
|
|
219
|
-
>
|
|
220
|
-
Cancel
|
|
221
|
-
</button>
|
|
222
|
-
|
|
223
|
-
<p className="pair-consent">
|
|
224
|
-
By pairing, you agree to our{" "}
|
|
225
|
-
<a href="https://www.palmier.me/terms" target="_blank" rel="noopener noreferrer">Terms of Service</a> and{" "}
|
|
226
|
-
<a href="https://www.palmier.me/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.
|
|
227
|
-
</p>
|
|
228
|
-
</div>
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
);
|
|
232
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
|
-
import { useNavigate } from "react-router-dom";
|
|
4
|
-
import { useHostConnection } from "../contexts/HostConnectionContext";
|
|
5
|
-
import { useHostStore } from "../contexts/HostStoreContext";
|
|
6
|
-
import CapabilityToggles from "../components/CapabilityToggles";
|
|
7
|
-
import { Device } from "../native/Device";
|
|
8
|
-
|
|
9
|
-
interface HostInfoResponse {
|
|
10
|
-
lan_url?: string | null;
|
|
11
|
-
linked_client_token?: string | null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type Phase = "loading" | "confirming" | "linking" | "wizard" | "linkError";
|
|
15
|
-
|
|
16
|
-
export default function PairSetup() {
|
|
17
|
-
const navigate = useNavigate();
|
|
18
|
-
const { connected, request, activeHost } = useHostConnection();
|
|
19
|
-
const { setHostLanUrl, pairedHosts } = useHostStore();
|
|
20
|
-
const isFirstHost = pairedHosts.length <= 1;
|
|
21
|
-
|
|
22
|
-
const [phase, setPhase] = useState<Phase>("loading");
|
|
23
|
-
const [linkedClientToken, setLinkedClientToken] = useState<string | null>(null);
|
|
24
|
-
const [linkError, setLinkError] = useState<string | null>(null);
|
|
25
|
-
|
|
26
|
-
function goToHost() {
|
|
27
|
-
navigate(`/hosts/${encodeURIComponent(activeHost.hostId)}`, { replace: true });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Phase: loading → fetch host.info, then transition.
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
if (!connected || phase !== "loading") return;
|
|
33
|
-
let cancelled = false;
|
|
34
|
-
request<HostInfoResponse>("host.info").catch(() => ({} as HostInfoResponse)).then((info) => {
|
|
35
|
-
if (cancelled) return;
|
|
36
|
-
setHostLanUrl(activeHost.hostId, info.lan_url ?? undefined);
|
|
37
|
-
const linked = info.linked_client_token ?? null;
|
|
38
|
-
setLinkedClientToken(linked);
|
|
39
|
-
const otherDeviceLinked = !!linked && linked !== activeHost.clientToken;
|
|
40
|
-
setPhase(otherDeviceLinked ? "confirming" : "linking");
|
|
41
|
-
});
|
|
42
|
-
return () => { cancelled = true; };
|
|
43
|
-
}, [connected, phase, activeHost, request, setHostLanUrl]);
|
|
44
|
-
|
|
45
|
-
// Phase: linking → call device.link, then either show wizard (first host)
|
|
46
|
-
// or navigate (subsequent host).
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (phase !== "linking") return;
|
|
49
|
-
let cancelled = false;
|
|
50
|
-
(async () => {
|
|
51
|
-
try {
|
|
52
|
-
if (Device) {
|
|
53
|
-
const { token: fcmToken } = await Device.getFcmToken();
|
|
54
|
-
if (!fcmToken) throw new Error("Could not read FCM token");
|
|
55
|
-
await request("device.link", { fcmToken });
|
|
56
|
-
}
|
|
57
|
-
if (cancelled) return;
|
|
58
|
-
if (isFirstHost) setPhase("wizard");
|
|
59
|
-
else goToHost();
|
|
60
|
-
} catch (err) {
|
|
61
|
-
if (cancelled) return;
|
|
62
|
-
setLinkError(err instanceof Error ? err.message : String(err));
|
|
63
|
-
setPhase("linkError");
|
|
64
|
-
}
|
|
65
|
-
})();
|
|
66
|
-
return () => { cancelled = true; };
|
|
67
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
68
|
-
}, [phase]);
|
|
69
|
-
|
|
70
|
-
function confirmLink() { setPhase("linking"); }
|
|
71
|
-
function cancelLink() { goToHost(); }
|
|
72
|
-
function retryLink() { setLinkError(null); setPhase("linking"); }
|
|
73
|
-
|
|
74
|
-
const linkModal = phase === "confirming" && createPortal(
|
|
75
|
-
<div className="confirm-modal-overlay" onClick={cancelLink}>
|
|
76
|
-
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
|
77
|
-
<h2 className="confirm-modal-title">Link the host to this device?</h2>
|
|
78
|
-
<p className="confirm-modal-message">
|
|
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
|
-
</p>
|
|
81
|
-
<div className="confirm-modal-actions">
|
|
82
|
-
<button className="btn btn-secondary" onClick={cancelLink}>Cancel</button>
|
|
83
|
-
<button className="btn btn-primary" onClick={confirmLink}>Link</button>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
</div>,
|
|
87
|
-
document.body,
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
if (phase === "loading" || phase === "confirming" || phase === "linking" || phase === "linkError") {
|
|
91
|
-
const isWizardCandidate = isFirstHost;
|
|
92
|
-
const showSpinner = phase === "loading" || phase === "confirming" || phase === "linking";
|
|
93
|
-
return (
|
|
94
|
-
<div className="pair-setup">
|
|
95
|
-
<div className="pair-setup-inner">
|
|
96
|
-
{isWizardCandidate && <h1 className="pair-setup-title">Device Capabilities</h1>}
|
|
97
|
-
<div className="pair-setup-loading">
|
|
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>}
|
|
102
|
-
{phase === "linkError" && (
|
|
103
|
-
<>
|
|
104
|
-
<p className="pair-error">{linkError}</p>
|
|
105
|
-
<button className="btn btn-primary" onClick={retryLink}>Retry</button>
|
|
106
|
-
<button className="btn btn-secondary" onClick={goToHost}>Skip linking</button>
|
|
107
|
-
</>
|
|
108
|
-
)}
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
{linkModal}
|
|
112
|
-
</div>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// phase === "wizard"
|
|
117
|
-
void linkedClientToken;
|
|
118
|
-
return (
|
|
119
|
-
<div className="pair-setup">
|
|
120
|
-
<div className="pair-setup-inner">
|
|
121
|
-
<h1 className="pair-setup-title">Device Capabilities</h1>
|
|
122
|
-
<p className="pair-setup-description">
|
|
123
|
-
Choose what the host can use this device for. You can change these later from the menu.
|
|
124
|
-
</p>
|
|
125
|
-
|
|
126
|
-
<CapabilityToggles />
|
|
127
|
-
|
|
128
|
-
<div className="pair-setup-actions">
|
|
129
|
-
<button className="btn btn-primary btn-full" onClick={goToHost}>Finish</button>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
);
|
|
134
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/// <reference lib="webworker" />
|
|
2
|
-
import { precacheAndRoute } from "workbox-precaching";
|
|
3
|
-
|
|
4
|
-
declare const self: ServiceWorkerGlobalScope;
|
|
5
|
-
|
|
6
|
-
// Precache assets injected by vite-plugin-pwa
|
|
7
|
-
precacheAndRoute(self.__WB_MANIFEST);
|
|
8
|
-
|
|
9
|
-
const API_URL = "/api/push/respond";
|
|
10
|
-
|
|
11
|
-
// hostId stored for potential future use (e.g., scoped push responses)
|
|
12
|
-
self.addEventListener("message", (_event) => {
|
|
13
|
-
// Handle messages from the main app (e.g., set-host-id)
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
self.addEventListener("push", (event) => {
|
|
17
|
-
if (!event.data) return;
|
|
18
|
-
|
|
19
|
-
let payload: {
|
|
20
|
-
title?: string;
|
|
21
|
-
body?: string;
|
|
22
|
-
data?: Record<string, unknown>;
|
|
23
|
-
type?: string;
|
|
24
|
-
};
|
|
25
|
-
try {
|
|
26
|
-
payload = event.data.json();
|
|
27
|
-
} catch {
|
|
28
|
-
payload = { title: "Palmier", body: event.data.text() };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const type = payload.type ?? (payload.data as Record<string, unknown>)?.type;
|
|
32
|
-
|
|
33
|
-
// Silent dismiss: close matching notification without showing a new one
|
|
34
|
-
if (type === "confirm-dismiss" || type === "permission-dismiss" || type === "input-dismiss") {
|
|
35
|
-
const data = payload.data ?? payload;
|
|
36
|
-
const dataHostId = (data as Record<string, unknown>).host_id;
|
|
37
|
-
const requestId = (data as Record<string, unknown>).session_id;
|
|
38
|
-
const taskId = (data as Record<string, unknown>).task_id; // permission-dismiss still uses task_id
|
|
39
|
-
event.waitUntil(
|
|
40
|
-
self.registration.getNotifications().then((notifications) => {
|
|
41
|
-
for (const n of notifications) {
|
|
42
|
-
if (n.data?.host_id !== dataHostId) continue;
|
|
43
|
-
if (requestId && n.data?.session_id === requestId) { n.close(); continue; }
|
|
44
|
-
if (taskId && n.data?.task_id === taskId) { n.close(); } // permission only
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const title = payload.title ?? "Palmier";
|
|
52
|
-
let body = payload.body ?? "";
|
|
53
|
-
if (!body && type === "confirm") {
|
|
54
|
-
body = "A task requires confirmation to run.";
|
|
55
|
-
}
|
|
56
|
-
if (!body && type === "permission") {
|
|
57
|
-
body = "A task needs additional permissions to continue.";
|
|
58
|
-
}
|
|
59
|
-
if (!body && type === "input") {
|
|
60
|
-
body = "A task needs your input to continue.";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const options: NotificationOptions & { vibrate?: number[]; actions?: Array<{ action: string; title: string }> } = {
|
|
64
|
-
body,
|
|
65
|
-
icon: "/pwa-192x192.png",
|
|
66
|
-
badge: "/pwa-192x192.png",
|
|
67
|
-
data: payload.data ?? payload,
|
|
68
|
-
vibrate: [100, 50, 100],
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Add action buttons for confirmation notifications
|
|
72
|
-
if (type === "confirm") {
|
|
73
|
-
options.actions = [
|
|
74
|
-
{ action: "confirm", title: "Confirm" },
|
|
75
|
-
{ action: "abort", title: "Abort" },
|
|
76
|
-
];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
event.waitUntil(self.registration.showNotification(title, options));
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
self.addEventListener("notificationclick", (event) => {
|
|
83
|
-
const notification = event.notification;
|
|
84
|
-
notification.close();
|
|
85
|
-
|
|
86
|
-
const data = notification.data ?? {};
|
|
87
|
-
const action = event.action;
|
|
88
|
-
|
|
89
|
-
if (action && data.type === "confirm" && data.session_id && data.host_id) {
|
|
90
|
-
const response = action === "confirm" ? "confirmed" : "aborted";
|
|
91
|
-
|
|
92
|
-
event.waitUntil(
|
|
93
|
-
fetch(API_URL, {
|
|
94
|
-
method: "POST",
|
|
95
|
-
headers: { "Content-Type": "application/json" },
|
|
96
|
-
body: JSON.stringify({
|
|
97
|
-
session_id: data.session_id,
|
|
98
|
-
host_id: data.host_id,
|
|
99
|
-
response,
|
|
100
|
-
}),
|
|
101
|
-
}).catch((err) => {
|
|
102
|
-
console.error("Failed to send push response:", err);
|
|
103
|
-
})
|
|
104
|
-
);
|
|
105
|
-
} else {
|
|
106
|
-
// User tapped the notification body — open the PWA.
|
|
107
|
-
// For task-complete/fail notifications, deep-link to the result view
|
|
108
|
-
// scoped to the originating host so the PWA switches hosts automatically.
|
|
109
|
-
const hostId = data.host_id;
|
|
110
|
-
const taskId = data.task_id;
|
|
111
|
-
const runId = data.run_id;
|
|
112
|
-
const hostPrefix = hostId ? `/hosts/${encodeURIComponent(hostId)}` : "";
|
|
113
|
-
const targetUrl = hostPrefix && taskId && runId
|
|
114
|
-
? `${hostPrefix}/runs/${encodeURIComponent(taskId)}/${encodeURIComponent(runId)}`
|
|
115
|
-
: hostPrefix && taskId
|
|
116
|
-
? `${hostPrefix}/runs/${encodeURIComponent(taskId)}/latest`
|
|
117
|
-
: hostPrefix || "/";
|
|
118
|
-
|
|
119
|
-
event.waitUntil(
|
|
120
|
-
self.clients
|
|
121
|
-
.matchAll({ type: "window", includeUncontrolled: true })
|
|
122
|
-
.then((clients) => {
|
|
123
|
-
for (const client of clients) {
|
|
124
|
-
if (client.url.includes(self.location.origin) && "focus" in client) {
|
|
125
|
-
(client as WindowClient).navigate(targetUrl);
|
|
126
|
-
return client.focus();
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return self.clients.openWindow(targetUrl);
|
|
130
|
-
})
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Activate immediately
|
|
136
|
-
self.addEventListener("install", () => {
|
|
137
|
-
self.skipWaiting();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
self.addEventListener("activate", (event) => {
|
|
141
|
-
event.waitUntil(self.clients.claim());
|
|
142
|
-
});
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
export interface AgentInfo {
|
|
2
|
-
key: string;
|
|
3
|
-
label: string;
|
|
4
|
-
supportsPermissions: boolean;
|
|
5
|
-
supportsYolo: boolean;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export interface Task {
|
|
10
|
-
id: string;
|
|
11
|
-
name: string;
|
|
12
|
-
user_prompt: string;
|
|
13
|
-
agent?: string;
|
|
14
|
-
schedule_type?: "crons" | "specific_times" | "on_new_notification" | "on_new_sms";
|
|
15
|
-
schedule_values?: string[];
|
|
16
|
-
schedule_enabled: boolean;
|
|
17
|
-
requires_confirmation: boolean;
|
|
18
|
-
yolo_mode?: boolean;
|
|
19
|
-
foreground_mode?: boolean;
|
|
20
|
-
permissions?: RequiredPermission[];
|
|
21
|
-
command?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface RequiredPermission {
|
|
25
|
-
name: string;
|
|
26
|
-
description: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface ConfirmNotification {
|
|
30
|
-
type: "confirm";
|
|
31
|
-
task_id: string;
|
|
32
|
-
host_id: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
|
|
36
|
-
|
|
37
|
-
export interface TaskStatus {
|
|
38
|
-
running_state: TaskRunningState;
|
|
39
|
-
/** UTC time in milliseconds since epoch */
|
|
40
|
-
time_stamp: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface HistoryEntry {
|
|
44
|
-
task_id: string;
|
|
45
|
-
run_id: string;
|
|
46
|
-
// Enriched by taskrun.list RPC from TASKRUN.md:
|
|
47
|
-
task_name?: string;
|
|
48
|
-
running_state?: string;
|
|
49
|
-
start_time?: number;
|
|
50
|
-
end_time?: number;
|
|
51
|
-
error?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface ConversationMessage {
|
|
55
|
-
role: "assistant" | "user" | "status";
|
|
56
|
-
time: number;
|
|
57
|
-
content: string;
|
|
58
|
-
type?: "input" | "permission" | "confirmation" | "monitoring" | "started" | "finished" | "failed" | "aborted" | "stopped" | "error";
|
|
59
|
-
attachments?: string[];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** A host paired via pairing code (stored in localStorage). */
|
|
63
|
-
export interface PairedHost {
|
|
64
|
-
hostId: string;
|
|
65
|
-
clientToken: string;
|
|
66
|
-
name?: string;
|
|
67
|
-
/** If set, all communication uses HTTP to this URL instead of NATS. Set only for loopback (Local mode). */
|
|
68
|
-
directUrl?: string;
|
|
69
|
-
/** Host's LAN URL, refreshed from each `host.info` response so laptop/DHCP IP changes propagate. Native Capacitor app probes for reachability and routes RPC over HTTP when reachable; events stay on NATS. */
|
|
70
|
-
lanUrl?: string;
|
|
71
|
-
/** Last-used agent key for this host. Seeds the agent picker on the session composer and task form. */
|
|
72
|
-
lastAgent?: string;
|
|
73
|
-
/** IANA timezone from the host, refreshed on each `host.info`. Drives all time rendering. */
|
|
74
|
-
timezone?: string;
|
|
75
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"useDefineForClassFields": true,
|
|
5
|
-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
-
"module": "ESNext",
|
|
7
|
-
"skipLibCheck": true,
|
|
8
|
-
"moduleResolution": "Bundler",
|
|
9
|
-
"allowImportingTsExtensions": true,
|
|
10
|
-
"isolatedModules": true,
|
|
11
|
-
"moduleDetection": "force",
|
|
12
|
-
"noEmit": true,
|
|
13
|
-
"jsx": "react-jsx",
|
|
14
|
-
"strict": true,
|
|
15
|
-
"noUnusedLocals": true,
|
|
16
|
-
"noUnusedParameters": true,
|
|
17
|
-
"noFallthroughCasesInSwitch": true,
|
|
18
|
-
"noUncheckedSideEffectImports": true
|
|
19
|
-
},
|
|
20
|
-
"include": ["src"]
|
|
21
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"lib": ["ES2023"],
|
|
5
|
-
"module": "ESNext",
|
|
6
|
-
"skipLibCheck": true,
|
|
7
|
-
"moduleResolution": "Bundler",
|
|
8
|
-
"allowImportingTsExtensions": true,
|
|
9
|
-
"isolatedModules": true,
|
|
10
|
-
"moduleDetection": "force",
|
|
11
|
-
"noEmit": true,
|
|
12
|
-
"strict": true,
|
|
13
|
-
"noUnusedLocals": true,
|
|
14
|
-
"noUnusedParameters": true,
|
|
15
|
-
"noFallthroughCasesInSwitch": true,
|
|
16
|
-
"noUncheckedSideEffectImports": true
|
|
17
|
-
},
|
|
18
|
-
"include": ["vite.config.ts"]
|
|
19
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "vite";
|
|
2
|
-
import react from "@vitejs/plugin-react";
|
|
3
|
-
import { VitePWA } from "vite-plugin-pwa";
|
|
4
|
-
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
server: {
|
|
7
|
-
host: true,
|
|
8
|
-
proxy: {
|
|
9
|
-
"/api": process.env.API_URL || "http://localhost:3000"
|
|
10
|
-
},
|
|
11
|
-
},
|
|
12
|
-
plugins: [
|
|
13
|
-
react(),
|
|
14
|
-
VitePWA({
|
|
15
|
-
strategies: "injectManifest",
|
|
16
|
-
srcDir: "src",
|
|
17
|
-
filename: "service-worker.ts",
|
|
18
|
-
registerType: "autoUpdate",
|
|
19
|
-
devOptions: {
|
|
20
|
-
enabled: true,
|
|
21
|
-
type: "module",
|
|
22
|
-
},
|
|
23
|
-
includeAssets: ["favicon.ico", "apple-touch-icon.png"],
|
|
24
|
-
manifest: {
|
|
25
|
-
name: "Palmier",
|
|
26
|
-
short_name: "Palmier",
|
|
27
|
-
description: "Bridge your AI agents and your phone. Your AI agents use your phone as a tool — GPS, email, calendar, contacts — and you use your phone as an agent remote.",
|
|
28
|
-
start_url: "/",
|
|
29
|
-
display: "standalone",
|
|
30
|
-
background_color: "#ffffff",
|
|
31
|
-
theme_color: "#2E5CE5",
|
|
32
|
-
icons: [
|
|
33
|
-
{
|
|
34
|
-
src: "pwa-192x192.png",
|
|
35
|
-
sizes: "192x192",
|
|
36
|
-
type: "image/png",
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
src: "pwa-512x512.png",
|
|
40
|
-
sizes: "512x512",
|
|
41
|
-
type: "image/png",
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
}),
|
|
46
|
-
],
|
|
47
|
-
});
|