desktop-pet-app 0.1.1

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.
@@ -0,0 +1,377 @@
1
+ const ARTIFACT_CHUNK_BYTES = 64 * 1024;
2
+ const P2P_CONNECT_TIMEOUT_MS = 12e3;
3
+ const P2P_CHAT_CONNECT_TIMEOUT_MS = 2500;
4
+ const MAGIC = new Uint8Array([68, 80, 69, 84]);
5
+ function encodeArtifactChunk(chunk) {
6
+ const id = new TextEncoder().encode(chunk.artifactId);
7
+ if (id.length === 0) throw new Error("artifact id must not be empty");
8
+ if (id.length > 128) throw new Error("artifact id is too long");
9
+ if (chunk.chunkIndex < 0 || chunk.totalChunks < 1 || chunk.chunkIndex >= chunk.totalChunks) {
10
+ throw new Error("invalid chunk indices");
11
+ }
12
+ if (chunk.payload.length > ARTIFACT_CHUNK_BYTES) {
13
+ throw new Error(`artifact chunk payload exceeds ${ARTIFACT_CHUNK_BYTES} bytes`);
14
+ }
15
+ const headerLength = 4 + 1 + 2 + 4 + 4 + 4 + id.length;
16
+ const output = new Uint8Array(headerLength + chunk.payload.length);
17
+ output.set(MAGIC, 0);
18
+ const view = new DataView(output.buffer);
19
+ view.setUint8(4, 1);
20
+ view.setUint16(5, id.length);
21
+ view.setUint32(7, chunk.chunkIndex);
22
+ view.setUint32(11, chunk.totalChunks);
23
+ view.setUint32(15, chunk.payload.length);
24
+ output.set(id, 19);
25
+ output.set(chunk.payload, headerLength);
26
+ return output.buffer;
27
+ }
28
+ function decodeArtifactChunk(buffer) {
29
+ const bytes = new Uint8Array(buffer);
30
+ if (bytes.length < 19 || !MAGIC.every((value, index) => bytes[index] === value)) {
31
+ throw new Error("invalid artifact chunk magic");
32
+ }
33
+ const view = new DataView(buffer);
34
+ if (view.getUint8(4) !== 1) throw new Error("unsupported artifact chunk version");
35
+ const idLength = view.getUint16(5);
36
+ if (idLength === 0) throw new Error("artifact id must not be empty");
37
+ if (idLength > 128) throw new Error("artifact id is too long");
38
+ const chunkIndex = view.getUint32(7);
39
+ const totalChunks = view.getUint32(11);
40
+ const payloadLength = view.getUint32(15);
41
+ if (payloadLength > ARTIFACT_CHUNK_BYTES) {
42
+ throw new Error(`artifact chunk payload exceeds ${ARTIFACT_CHUNK_BYTES} bytes`);
43
+ }
44
+ const headerLength = 19 + idLength;
45
+ if (totalChunks < 1 || chunkIndex >= totalChunks || bytes.length !== headerLength + payloadLength) {
46
+ throw new Error("invalid artifact chunk lengths");
47
+ }
48
+ return {
49
+ artifactId: new TextDecoder().decode(bytes.slice(19, headerLength)),
50
+ chunkIndex,
51
+ totalChunks,
52
+ payload: bytes.slice(headerLength)
53
+ };
54
+ }
55
+ class IceCandidateBuffer {
56
+ constructor(maxPerSession = 64) {
57
+ this.maxPerSession = maxPerSession;
58
+ }
59
+ candidates = /* @__PURE__ */ new Map();
60
+ add(sessionId, candidate) {
61
+ const queued = this.candidates.get(sessionId) ?? [];
62
+ if (queued.length < this.maxPerSession) queued.push(candidate);
63
+ this.candidates.set(sessionId, queued);
64
+ }
65
+ take(sessionId) {
66
+ const queued = this.candidates.get(sessionId) ?? [];
67
+ this.candidates.delete(sessionId);
68
+ return queued;
69
+ }
70
+ clear(sessionId) {
71
+ this.candidates.delete(sessionId);
72
+ }
73
+ }
74
+ class ReliableArtifactChannel {
75
+ constructor(channel) {
76
+ this.channel = channel;
77
+ channel.binaryType = "arraybuffer";
78
+ channel.bufferedAmountLowThreshold = ARTIFACT_CHUNK_BYTES * 4;
79
+ channel.addEventListener("message", (event) => void this.receive(event.data));
80
+ }
81
+ incoming = /* @__PURE__ */ new Map();
82
+ outgoing = /* @__PURE__ */ new Map();
83
+ onArtifact = () => {
84
+ };
85
+ onControl = () => {
86
+ };
87
+ async sendArtifact(artifactId, bytes) {
88
+ const totalChunks = Math.max(1, Math.ceil(bytes.length / ARTIFACT_CHUNK_BYTES));
89
+ const sha256 = await digest(bytes);
90
+ const chunks = Array.from({ length: totalChunks }, (_, index) => {
91
+ const start = index * ARTIFACT_CHUNK_BYTES;
92
+ return bytes.slice(start, Math.min(bytes.length, start + ARTIFACT_CHUNK_BYTES));
93
+ });
94
+ this.outgoing.set(artifactId, { chunks, acknowledged: /* @__PURE__ */ new Set() });
95
+ this.sendControl({ type: "artifact.start", artifactId, size: bytes.length, sha256, totalChunks });
96
+ await this.sendMissing(artifactId);
97
+ }
98
+ async receive(value) {
99
+ if (typeof value === "string") {
100
+ const control = JSON.parse(value);
101
+ this.onControl(control);
102
+ if (control.type === "artifact.start") {
103
+ this.incoming.set(control.artifactId, {
104
+ size: control.size,
105
+ sha256: control.sha256,
106
+ totalChunks: control.totalChunks,
107
+ chunks: /* @__PURE__ */ new Map()
108
+ });
109
+ } else if (control.type === "artifact.chunk-ack") {
110
+ const outgoing = this.outgoing.get(control.artifactId);
111
+ for (const index of control.received) outgoing?.acknowledged.add(index);
112
+ } else if (control.type === "artifact.resume") {
113
+ const outgoing = this.outgoing.get(control.artifactId);
114
+ if (outgoing) {
115
+ outgoing.acknowledged = new Set(control.received);
116
+ await this.sendMissing(control.artifactId);
117
+ }
118
+ } else if (control.type === "artifact.hash-mismatch") {
119
+ const outgoing = this.outgoing.get(control.artifactId);
120
+ if (outgoing) {
121
+ outgoing.acknowledged.clear();
122
+ await this.sendMissing(control.artifactId);
123
+ }
124
+ } else if (control.type === "artifact.complete" || control.type === "artifact.abort") {
125
+ this.outgoing.delete(control.artifactId);
126
+ }
127
+ return;
128
+ }
129
+ if (!(value instanceof ArrayBuffer)) return;
130
+ const chunk = decodeArtifactChunk(value);
131
+ const artifact = this.incoming.get(chunk.artifactId);
132
+ if (!artifact || artifact.totalChunks !== chunk.totalChunks) return;
133
+ artifact.chunks.set(chunk.chunkIndex, chunk.payload);
134
+ if (!artifact.ackTimer) {
135
+ artifact.ackTimer = setTimeout(() => {
136
+ artifact.ackTimer = void 0;
137
+ this.acknowledge(chunk.artifactId, artifact);
138
+ }, 500);
139
+ }
140
+ if (artifact.chunks.size % 16 === 0 || artifact.chunks.size === artifact.totalChunks) {
141
+ if (artifact.ackTimer) clearTimeout(artifact.ackTimer);
142
+ artifact.ackTimer = void 0;
143
+ this.acknowledge(chunk.artifactId, artifact);
144
+ }
145
+ if (artifact.chunks.size !== artifact.totalChunks) return;
146
+ const output = new Uint8Array(artifact.size);
147
+ let offset = 0;
148
+ for (let index = 0; index < artifact.totalChunks; index++) {
149
+ const part = artifact.chunks.get(index);
150
+ if (!part) return;
151
+ output.set(part, offset);
152
+ offset += part.length;
153
+ }
154
+ this.incoming.delete(chunk.artifactId);
155
+ if (offset !== artifact.size || await digest(output) !== artifact.sha256) {
156
+ this.sendControl({ type: "artifact.hash-mismatch", artifactId: chunk.artifactId });
157
+ return;
158
+ }
159
+ this.sendControl({ type: "artifact.complete", artifactId: chunk.artifactId });
160
+ this.onArtifact(chunk.artifactId, output);
161
+ }
162
+ resumeArtifact(artifactId) {
163
+ const artifact = this.incoming.get(artifactId);
164
+ this.sendControl({
165
+ type: "artifact.resume",
166
+ artifactId,
167
+ received: artifact ? [...artifact.chunks.keys()].sort((a, b) => a - b) : []
168
+ });
169
+ }
170
+ acknowledge(artifactId, artifact) {
171
+ this.sendControl({
172
+ type: "artifact.chunk-ack",
173
+ artifactId,
174
+ received: [...artifact.chunks.keys()].sort((a, b) => a - b)
175
+ });
176
+ }
177
+ async sendMissing(artifactId) {
178
+ const outgoing = this.outgoing.get(artifactId);
179
+ if (!outgoing) return;
180
+ for (let index = 0; index < outgoing.chunks.length; index++) {
181
+ if (outgoing.acknowledged.has(index)) continue;
182
+ await this.waitForBackpressure();
183
+ this.channel.send(
184
+ encodeArtifactChunk({
185
+ artifactId,
186
+ chunkIndex: index,
187
+ totalChunks: outgoing.chunks.length,
188
+ payload: outgoing.chunks[index]
189
+ })
190
+ );
191
+ }
192
+ }
193
+ sendControl(control) {
194
+ this.channel.send(JSON.stringify(control));
195
+ }
196
+ waitForBackpressure() {
197
+ if (this.channel.bufferedAmount <= this.channel.bufferedAmountLowThreshold) return Promise.resolve();
198
+ return new Promise((resolve) => this.channel.addEventListener("bufferedamountlow", () => resolve(), { once: true }));
199
+ }
200
+ }
201
+ async function digest(bytes) {
202
+ const copy = new Uint8Array(bytes.byteLength);
203
+ copy.set(bytes);
204
+ const hash = await crypto.subtle.digest("SHA-256", copy.buffer);
205
+ return [...new Uint8Array(hash)].map((value) => value.toString(16).padStart(2, "0")).join("");
206
+ }
207
+ const __vite_import_meta_env__ = { "BASE_URL": "./", "DEV": false, "MODE": "production", "PROD": true, "SSR": false };
208
+ const sessions = /* @__PURE__ */ new Map();
209
+ const pendingIceCandidates = new IceCandidateBuffer();
210
+ let iceServers = parseIceServers();
211
+ window.p2pApi.onStart((request) => void createOffer(request));
212
+ window.p2pApi.onChatSend((request) => void createOffer(request));
213
+ window.p2pApi.onSignal((message) => {
214
+ void receiveSignal(message).catch(() => failSession(message.sessionId));
215
+ });
216
+ window.p2pApi.onConfig((config) => {
217
+ if (config.iceServers?.length) iceServers = config.iceServers;
218
+ });
219
+ async function createOffer(request) {
220
+ const sessionId = crypto.randomUUID();
221
+ const peer = createPeer(
222
+ request.taskId,
223
+ sessionId,
224
+ request.toDeviceId,
225
+ request.operationId ? P2P_CHAT_CONNECT_TIMEOUT_MS : P2P_CONNECT_TIMEOUT_MS
226
+ );
227
+ const channel = peer.createDataChannel("desktop-pet-artifacts", { ordered: true });
228
+ const session = sessions.get(sessionId);
229
+ session.channel = new ReliableArtifactChannel(channel);
230
+ session.operationId = request.operationId;
231
+ wireChatChannel(channel, request.toUsername, request.operationId, request.text);
232
+ const offer = await peer.createOffer();
233
+ await peer.setLocalDescription(offer);
234
+ send({ type: "p2p.offer", taskId: request.taskId, toDeviceId: request.toDeviceId, sessionId, signal: offer });
235
+ }
236
+ async function receiveSignal(message) {
237
+ if (new Date(message.expiresAt).getTime() <= Date.now()) return;
238
+ if (!await verifySignal(message)) return;
239
+ if (message.type === "p2p.offer" && message.fromDeviceId && message.signal) {
240
+ const peer = createPeer(message.taskId, message.sessionId, message.fromDeviceId);
241
+ peer.ondatachannel = (event) => {
242
+ sessions.get(message.sessionId).channel = new ReliableArtifactChannel(event.channel);
243
+ wireChatChannel(event.channel, message.from);
244
+ };
245
+ await peer.setRemoteDescription(message.signal);
246
+ await flushPendingIceCandidates(message.sessionId, peer);
247
+ const answer = await peer.createAnswer();
248
+ await peer.setLocalDescription(answer);
249
+ send({
250
+ type: "p2p.answer",
251
+ taskId: message.taskId,
252
+ sessionId: message.sessionId,
253
+ toDeviceId: message.fromDeviceId,
254
+ signal: answer
255
+ });
256
+ } else if (message.type === "p2p.answer" && message.signal) {
257
+ const peer = sessions.get(message.sessionId)?.peer;
258
+ if (!peer) return;
259
+ await peer.setRemoteDescription(message.signal);
260
+ await flushPendingIceCandidates(message.sessionId, peer);
261
+ } else if (message.type === "p2p.ice-candidate" && message.signal) {
262
+ const candidate = message.signal;
263
+ const peer = sessions.get(message.sessionId)?.peer;
264
+ if (peer?.remoteDescription) {
265
+ await peer.addIceCandidate(candidate);
266
+ } else {
267
+ pendingIceCandidates.add(message.sessionId, candidate);
268
+ }
269
+ } else if (message.type === "p2p.close" || message.type === "p2p.failed") {
270
+ closeSession(message.sessionId);
271
+ }
272
+ }
273
+ async function verifySignal(message) {
274
+ if (!message.signature || !message.publicKey || !message.fromDeviceId) return false;
275
+ try {
276
+ const der = pemToBytes(message.publicKey);
277
+ const key = await crypto.subtle.importKey("spki", der, { name: "Ed25519" }, false, ["verify"]);
278
+ const signalJSON = JSON.stringify(message.signal ?? null);
279
+ const digest2 = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(signalJSON));
280
+ const hash = [...new Uint8Array(digest2)].map((value) => value.toString(16).padStart(2, "0")).join("");
281
+ const payload = [
282
+ message.type,
283
+ message.taskId,
284
+ message.sessionId,
285
+ message.fromDeviceId,
286
+ message.toDeviceId,
287
+ message.expiresAt,
288
+ hash
289
+ ].join("\n");
290
+ return crypto.subtle.verify(
291
+ "Ed25519",
292
+ key,
293
+ Uint8Array.from(atob(message.signature), (char) => char.charCodeAt(0)),
294
+ new TextEncoder().encode(payload)
295
+ );
296
+ } catch {
297
+ return false;
298
+ }
299
+ }
300
+ function pemToBytes(value) {
301
+ const base64 = value.replace(/-----[^-]+-----/g, "").replace(/\s/g, "");
302
+ return Uint8Array.from(atob(base64), (char) => char.charCodeAt(0)).buffer;
303
+ }
304
+ function createPeer(taskId, sessionId, toDeviceId, timeoutMs = P2P_CONNECT_TIMEOUT_MS) {
305
+ const peer = new RTCPeerConnection({ iceServers });
306
+ const timer = setTimeout(() => {
307
+ const operationId = sessions.get(sessionId)?.operationId;
308
+ if (operationId) window.p2pApi.emitChatEvent({ type: "failed", operationId });
309
+ send({ type: "p2p.failed", taskId, sessionId, toDeviceId });
310
+ closeSession(sessionId);
311
+ }, timeoutMs);
312
+ sessions.set(sessionId, { peer, timer });
313
+ peer.onicecandidate = (event) => {
314
+ if (event.candidate) {
315
+ send({ type: "p2p.ice-candidate", taskId, sessionId, toDeviceId, signal: event.candidate.toJSON() });
316
+ }
317
+ };
318
+ peer.onconnectionstatechange = () => {
319
+ if (peer.connectionState === "connected") {
320
+ clearTimeout(timer);
321
+ send({ type: "p2p.ready", taskId, sessionId, toDeviceId });
322
+ } else if (peer.connectionState === "failed" || peer.connectionState === "closed") {
323
+ const operationId = sessions.get(sessionId)?.operationId;
324
+ if (operationId) window.p2pApi.emitChatEvent({ type: "failed", operationId });
325
+ closeSession(sessionId);
326
+ }
327
+ };
328
+ return peer;
329
+ }
330
+ function wireChatChannel(channel, peerUsername, operationId, outgoingText) {
331
+ if (operationId && outgoingText) {
332
+ channel.addEventListener("open", () => {
333
+ channel.send(JSON.stringify({ type: "chat.message", text: outgoingText }));
334
+ window.p2pApi.emitChatEvent({ type: "sent", operationId });
335
+ }, { once: true });
336
+ }
337
+ channel.addEventListener("message", (event) => {
338
+ if (typeof event.data !== "string") return;
339
+ try {
340
+ const message = JSON.parse(event.data);
341
+ if (message.type === "chat.message" && message.text) {
342
+ window.p2pApi.emitChatEvent({ type: "received", from: peerUsername ?? "好友", text: message.text });
343
+ }
344
+ } catch {
345
+ }
346
+ });
347
+ }
348
+ function closeSession(sessionId) {
349
+ const session = sessions.get(sessionId);
350
+ if (!session) return;
351
+ clearTimeout(session.timer);
352
+ session.peer.close();
353
+ sessions.delete(sessionId);
354
+ pendingIceCandidates.clear(sessionId);
355
+ }
356
+ function failSession(sessionId) {
357
+ const operationId = sessions.get(sessionId)?.operationId;
358
+ if (operationId) window.p2pApi.emitChatEvent({ type: "failed", operationId });
359
+ closeSession(sessionId);
360
+ pendingIceCandidates.clear(sessionId);
361
+ }
362
+ async function flushPendingIceCandidates(sessionId, peer) {
363
+ const candidates = pendingIceCandidates.take(sessionId);
364
+ for (const candidate of candidates) await peer.addIceCandidate(candidate);
365
+ }
366
+ function send(message) {
367
+ window.p2pApi.sendSignal({ ...message, expiresAt: new Date(Date.now() + 5 * 6e4).toISOString() });
368
+ }
369
+ function parseIceServers() {
370
+ try {
371
+ const env = __vite_import_meta_env__;
372
+ const configured = JSON.parse(env?.VITE_DESKTOP_PET_ICE_SERVERS ?? "[]");
373
+ return configured.length ? configured : [{ urls: "stun:stun.l.google.com:19302" }];
374
+ } catch {
375
+ return [{ urls: "stun:stun.l.google.com:19302" }];
376
+ }
377
+ }
@@ -0,0 +1,156 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Desktop Pet</title>
6
+ <link
7
+ href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
8
+ rel="stylesheet"
9
+ />
10
+ <style>
11
+ html,
12
+ body {
13
+ margin: 0;
14
+ width: 100%;
15
+ height: 100%;
16
+ background: transparent;
17
+ overflow: hidden;
18
+ -webkit-user-select: none;
19
+ user-select: none;
20
+ }
21
+
22
+ #pet {
23
+ position: absolute;
24
+ bottom: 6px;
25
+ left: 50%;
26
+ transform: translateX(-50%);
27
+ cursor: grab;
28
+ }
29
+ #pet:active {
30
+ cursor: grabbing;
31
+ }
32
+
33
+ .pet-identity {
34
+ position: absolute;
35
+ left: 50%;
36
+ bottom: 2px;
37
+ transform: translateX(-50%);
38
+ z-index: 8;
39
+ max-width: calc(100% - 12px);
40
+ box-sizing: border-box;
41
+ border: 1px solid rgba(255, 255, 255, 0.72);
42
+ border-radius: 999px;
43
+ padding: 3px 7px;
44
+ background: rgba(28, 31, 38, 0.88);
45
+ color: #ffffff;
46
+ font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif;
47
+ font-size: 10px;
48
+ font-weight: 650;
49
+ line-height: 1.2;
50
+ text-align: center;
51
+ white-space: nowrap;
52
+ overflow: hidden;
53
+ text-overflow: ellipsis;
54
+ pointer-events: none;
55
+ }
56
+ .pet-identity[data-kind='remote-friend'] {
57
+ background: rgba(21, 93, 160, 0.92);
58
+ }
59
+
60
+ .bubble {
61
+ position: absolute;
62
+ top: 6px;
63
+ left: 50%;
64
+ transform: translateX(-50%);
65
+ max-width: 176px;
66
+ box-sizing: border-box;
67
+ background: #ffffff;
68
+ border: 3px solid #2d2d2d;
69
+ border-radius: 10px;
70
+ padding: 6px 9px;
71
+ font-family: 'Press Start 2P', 'PingFang SC', monospace;
72
+ font-size: 10px;
73
+ line-height: 1.6;
74
+ color: #2d2d2d;
75
+ box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.18);
76
+ word-break: break-all;
77
+ pointer-events: none;
78
+ opacity: 0;
79
+ transition: opacity 0.3s;
80
+ }
81
+ .bubble::after {
82
+ content: '';
83
+ position: absolute;
84
+ bottom: -8px;
85
+ left: 50%;
86
+ margin-left: -5px;
87
+ width: 10px;
88
+ height: 10px;
89
+ background: #ffffff;
90
+ border-right: 3px solid #2d2d2d;
91
+ border-bottom: 3px solid #2d2d2d;
92
+ transform: rotate(45deg);
93
+ }
94
+ .bubble.show {
95
+ opacity: 1;
96
+ }
97
+ .bubble.fade {
98
+ opacity: 0;
99
+ }
100
+ .bubble.pop {
101
+ animation: pop 0.25s cubic-bezier(0.2, 1.6, 0.4, 1);
102
+ }
103
+ @keyframes pop {
104
+ 0% {
105
+ transform: translateX(-50%) scale(0.6);
106
+ }
107
+ 100% {
108
+ transform: translateX(-50%) scale(1);
109
+ }
110
+ }
111
+
112
+ .reward-float {
113
+ position: absolute;
114
+ left: 50%;
115
+ bottom: 72px;
116
+ transform: translateX(-50%);
117
+ font-family: 'Press Start 2P', 'PingFang SC', monospace;
118
+ font-size: 11px;
119
+ color: #f0a05a;
120
+ text-shadow:
121
+ 2px 2px 0 #1a1008,
122
+ 0 0 8px rgba(240, 160, 90, 0.55);
123
+ pointer-events: none;
124
+ opacity: 0;
125
+ white-space: nowrap;
126
+ z-index: 5;
127
+ }
128
+ .reward-float.play {
129
+ animation: fish-rise 2.1s cubic-bezier(0.22, 1, 0.36, 1) forwards;
130
+ }
131
+ @keyframes fish-rise {
132
+ 0% {
133
+ opacity: 0;
134
+ transform: translateX(-50%) translateY(12px) scale(0.7);
135
+ }
136
+ 18% {
137
+ opacity: 1;
138
+ transform: translateX(-50%) translateY(0) scale(1.08);
139
+ }
140
+ 70% {
141
+ opacity: 1;
142
+ transform: translateX(-50%) translateY(-28px) scale(1);
143
+ }
144
+ 100% {
145
+ opacity: 0;
146
+ transform: translateX(-50%) translateY(-48px) scale(0.95);
147
+ }
148
+ }
149
+ </style>
150
+ <script type="module" crossorigin src="./assets/index-DqBgThfZ.js"></script>
151
+ </head>
152
+ <body>
153
+ <canvas id="pet"></canvas>
154
+ <div id="pet-identity" class="pet-identity" hidden></div>
155
+ </body>
156
+ </html>
@@ -0,0 +1,6 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head><meta charset="UTF-8" /><title>Desktop Pet P2P</title> <script type="module" crossorigin src="./assets/p2p-Ch5g1MsK.js"></script>
4
+ </head>
5
+ <body></body>
6
+ </html>
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "desktop-pet-app",
3
+ "version": "0.1.1",
4
+ "description": "AI desktop pet with MCP support and a self-hosted relay server",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "desktop-pet",
8
+ "electron",
9
+ "mcp",
10
+ "ai-agent"
11
+ ],
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "main": "out/main/index.js",
19
+ "bin": {
20
+ "desktop-pet": "./bin/cli.js"
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "out"
25
+ ],
26
+ "scripts": {
27
+ "dev": "electron-vite dev",
28
+ "build": "electron-vite build && npm run build:mcp",
29
+ "build:mcp": "esbuild src/mcp-server/index.ts --bundle --platform=node --format=esm --packages=external --outfile=out/mcp-server/index.js && node -e \"require('fs').writeFileSync('out/mcp-server/package.json', JSON.stringify({ type: 'module' }))\"",
30
+ "e2e:duty": "node scripts/e2e-duty-sessions.mjs",
31
+ "e2e:codebuddy:git": "node scripts/e2e-codebuddy-git-task.mjs",
32
+ "e2e:codebuddy:context": "node scripts/e2e-codebuddy-context-round.mjs",
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "vitest run",
35
+ "package:mac": "npm run build && electron-builder --mac pkg --publish never",
36
+ "pack:check": "npm pack --dry-run",
37
+ "prepack": "npm run build",
38
+ "prepublishOnly": "npm run typecheck && npm test && npm run pack:check"
39
+ },
40
+ "build": {
41
+ "appId": "com.xinshu.desktoppet",
42
+ "productName": "Desktop Pet",
43
+ "directories": {
44
+ "output": "release"
45
+ },
46
+ "files": [
47
+ "out/**",
48
+ "bin/**",
49
+ "package.json"
50
+ ],
51
+ "mac": {
52
+ "category": "public.app-category.utilities",
53
+ "target": [
54
+ "pkg"
55
+ ]
56
+ }
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^22.0.0",
60
+ "@types/ws": "^8.18.1",
61
+ "electron-builder": "^26.15.3",
62
+ "electron-vite": "^2.3.0",
63
+ "typescript": "^5.6.3",
64
+ "vite": "^5.4.8",
65
+ "vitest": "^3.2.7"
66
+ },
67
+ "dependencies": {
68
+ "@modelcontextprotocol/sdk": "^1.30.0",
69
+ "electron": "^33.2.0",
70
+ "ws": "^8.21.3",
71
+ "zod": "^4.5.4"
72
+ }
73
+ }