@whaletech/pet 0.2.1 → 0.2.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.
- package/README.md +1 -1
- package/dist/main.js +208 -80
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ pet
|
|
|
22
22
|
|
|
23
23
|
**Server** (self-hosted): Bun + SQLite backend handling state, AI chat, and payments. Deploy separately.
|
|
24
24
|
|
|
25
|
-
The client connects to `https://pet.whaletech.
|
|
25
|
+
The client connects to `https://api.pet.whaletech.app` by default, with a fallback to `https://pet-api.whaletech.app`. Override with:
|
|
26
26
|
|
|
27
27
|
```
|
|
28
28
|
WHALE_API_URL=http://localhost:3001 pet
|
package/dist/main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// src/main.tsx
|
|
3
3
|
import { useState as useState10, useCallback as useCallback2 } from "react";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import { render } from "ink";
|
|
5
6
|
|
|
6
7
|
// src/storage.ts
|
|
@@ -409,35 +410,142 @@ function getCompanionFromStored(userId, stored) {
|
|
|
409
410
|
}
|
|
410
411
|
|
|
411
412
|
// src/api.ts
|
|
412
|
-
|
|
413
|
+
import { spawn } from "node:child_process";
|
|
414
|
+
var PRIMARY_API_URL = "https://api.pet.whaletech.app";
|
|
415
|
+
var FALLBACK_API_URL = "https://pet-api.whaletech.app";
|
|
416
|
+
var API_URLS = [
|
|
417
|
+
process.env["WHALE_API_URL"],
|
|
418
|
+
PRIMARY_API_URL,
|
|
419
|
+
FALLBACK_API_URL
|
|
420
|
+
].filter((url, index, urls) => Boolean(url) && urls.indexOf(url) === index);
|
|
421
|
+
var CURL = process.platform === "win32" ? "curl.exe" : "curl";
|
|
413
422
|
var _token = null;
|
|
414
423
|
function setToken(token) {
|
|
415
424
|
_token = token;
|
|
416
425
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
426
|
+
function isRetryableNetworkError(err) {
|
|
427
|
+
if (!(err instanceof Error))
|
|
428
|
+
return false;
|
|
429
|
+
const text = [err.message, err.cause instanceof Error ? err.cause.message : "", String(err.cause?.code ?? "")].join(" ").toUpperCase();
|
|
430
|
+
return ["FETCH FAILED", "ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"].some((token) => text.includes(token));
|
|
431
|
+
}
|
|
432
|
+
function runCurl(args, input) {
|
|
433
|
+
return new Promise((resolve, reject) => {
|
|
434
|
+
const child = spawn(CURL, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
435
|
+
let stdout = "";
|
|
436
|
+
let stderr = "";
|
|
437
|
+
child.stdout.on("data", (chunk) => {
|
|
438
|
+
stdout += String(chunk);
|
|
425
439
|
});
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
440
|
+
child.stderr.on("data", (chunk) => {
|
|
441
|
+
stderr += String(chunk);
|
|
442
|
+
});
|
|
443
|
+
child.on("error", reject);
|
|
444
|
+
child.on("close", (code) => {
|
|
445
|
+
if (code !== 0) {
|
|
446
|
+
reject(new Error(stderr.trim() || `${CURL} exited with code ${code}`));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const marker = `
|
|
450
|
+
__CURL_STATUS__:`;
|
|
451
|
+
const index = stdout.lastIndexOf(marker);
|
|
452
|
+
if (index === -1) {
|
|
453
|
+
reject(new Error("curl response missing status marker"));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const body = stdout.slice(0, index);
|
|
457
|
+
const status = Number(stdout.slice(index + marker.length).trim());
|
|
458
|
+
resolve({ status, body });
|
|
459
|
+
});
|
|
460
|
+
child.stdin.end(input);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
async function postWithCurl(apiUrl, path2, body) {
|
|
464
|
+
const requestBody = _token ? { ...body, token: _token } : body;
|
|
465
|
+
const response = await runCurl(["-sS", "-X", "POST", `${apiUrl}${path2}`, "-H", "Content-Type: application/json", "--data-binary", "@-", "-w", `
|
|
466
|
+
__CURL_STATUS__:%{http_code}`], JSON.stringify(requestBody));
|
|
467
|
+
const data = JSON.parse(response.body);
|
|
468
|
+
if (response.status < 200 || response.status >= 300) {
|
|
469
|
+
const isQuotaExceeded = data.error === "quota_exceeded";
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
error: String(data.error ?? `HTTP ${response.status}`),
|
|
473
|
+
quotaExceeded: isQuotaExceeded,
|
|
474
|
+
paymentType: isQuotaExceeded ? String(data.paymentType) : undefined
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return { ok: true, data };
|
|
478
|
+
}
|
|
479
|
+
async function sendChatStreamWithCurl(apiUrl, body, onToken) {
|
|
480
|
+
const requestBody = _token ? { ...body, token: _token } : body;
|
|
481
|
+
const response = await runCurl(["-sS", "-N", "-X", "POST", `${apiUrl}/api/chat`, "-H", "Content-Type: application/json", "--data-binary", "@-", "-w", `
|
|
482
|
+
__CURL_STATUS__:%{http_code}`], JSON.stringify(requestBody));
|
|
483
|
+
if (response.status < 200 || response.status >= 300) {
|
|
484
|
+
const data = JSON.parse(response.body);
|
|
485
|
+
const isQuotaExceeded = data.error === "quota_exceeded";
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
error: String(data.error ?? `HTTP ${response.status}`),
|
|
489
|
+
quotaExceeded: isQuotaExceeded,
|
|
490
|
+
paymentType: isQuotaExceeded ? String(data.paymentType) : undefined
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
let finalState = null;
|
|
494
|
+
let finalQuota = null;
|
|
495
|
+
for (const line of response.body.split(`
|
|
496
|
+
`)) {
|
|
497
|
+
const trimmed = line.trim();
|
|
498
|
+
if (!trimmed || !trimmed.startsWith("data: "))
|
|
499
|
+
continue;
|
|
500
|
+
try {
|
|
501
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
502
|
+
if (data.token)
|
|
503
|
+
onToken(data.token);
|
|
504
|
+
if (data.done && data.state && data.quota) {
|
|
505
|
+
finalState = data.state;
|
|
506
|
+
finalQuota = data.quota;
|
|
507
|
+
}
|
|
508
|
+
} catch {}
|
|
509
|
+
}
|
|
510
|
+
if (finalState && finalQuota) {
|
|
511
|
+
return { ok: true, state: finalState, quota: finalQuota };
|
|
512
|
+
}
|
|
513
|
+
return { ok: false, error: "stream ended without state" };
|
|
514
|
+
}
|
|
515
|
+
async function post(path2, body) {
|
|
516
|
+
let lastError = "unknown error";
|
|
517
|
+
for (const apiUrl of API_URLS) {
|
|
518
|
+
try {
|
|
519
|
+
const requestBody = _token ? { ...body, token: _token } : body;
|
|
520
|
+
const res = await fetch(`${apiUrl}${path2}`, {
|
|
521
|
+
method: "POST",
|
|
522
|
+
headers: { "Content-Type": "application/json" },
|
|
523
|
+
body: JSON.stringify(requestBody)
|
|
524
|
+
});
|
|
525
|
+
const data = await res.json();
|
|
526
|
+
if (!res.ok) {
|
|
527
|
+
const isQuotaExceeded = data.error === "quota_exceeded";
|
|
528
|
+
return {
|
|
529
|
+
ok: false,
|
|
530
|
+
error: String(data.error ?? `HTTP ${res.status}`),
|
|
531
|
+
quotaExceeded: isQuotaExceeded,
|
|
532
|
+
paymentType: isQuotaExceeded ? String(data.paymentType) : undefined
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
return { ok: true, data };
|
|
536
|
+
} catch (err) {
|
|
537
|
+
if (isRetryableNetworkError(err)) {
|
|
538
|
+
try {
|
|
539
|
+
return await postWithCurl(apiUrl, path2, body);
|
|
540
|
+
} catch (curlErr) {
|
|
541
|
+
lastError = curlErr instanceof Error ? curlErr.message : "unknown error";
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
lastError = err instanceof Error ? err.message : "unknown error";
|
|
435
546
|
}
|
|
436
|
-
return { ok: true, data };
|
|
437
|
-
} catch (err) {
|
|
438
|
-
const msg = err instanceof Error ? err.message : "unknown error";
|
|
439
|
-
return { ok: false, error: msg };
|
|
440
547
|
}
|
|
548
|
+
return { ok: false, error: lastError };
|
|
441
549
|
}
|
|
442
550
|
function syncState(userId, localState) {
|
|
443
551
|
return post("/api/sync", { userId, localState });
|
|
@@ -452,69 +560,88 @@ async function persistState(userId, state, quota) {
|
|
|
452
560
|
return result;
|
|
453
561
|
}
|
|
454
562
|
async function sendChatStream(userId, companion, state, message, onToken) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
body.token = _token;
|
|
467
|
-
const res = await fetch(`${API_URL}/api/chat`, {
|
|
468
|
-
method: "POST",
|
|
469
|
-
headers: { "Content-Type": "application/json" },
|
|
470
|
-
body: JSON.stringify(body)
|
|
471
|
-
});
|
|
472
|
-
if (!res.ok) {
|
|
473
|
-
const data = await res.json();
|
|
474
|
-
const isQuotaExceeded = data.error === "quota_exceeded";
|
|
475
|
-
return {
|
|
476
|
-
ok: false,
|
|
477
|
-
error: String(data.error ?? `HTTP ${res.status}`),
|
|
478
|
-
quotaExceeded: isQuotaExceeded,
|
|
479
|
-
paymentType: isQuotaExceeded ? String(data.paymentType) : undefined
|
|
563
|
+
let lastError = "unknown error";
|
|
564
|
+
for (const apiUrl of API_URLS) {
|
|
565
|
+
try {
|
|
566
|
+
const body = {
|
|
567
|
+
userId,
|
|
568
|
+
name: companion.name,
|
|
569
|
+
personality: companion.personality,
|
|
570
|
+
species: companion.species,
|
|
571
|
+
mood: deriveMood(state),
|
|
572
|
+
needs: state.needs,
|
|
573
|
+
message
|
|
480
574
|
};
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
575
|
+
if (_token)
|
|
576
|
+
body.token = _token;
|
|
577
|
+
const res = await fetch(`${apiUrl}/api/chat`, {
|
|
578
|
+
method: "POST",
|
|
579
|
+
headers: { "Content-Type": "application/json" },
|
|
580
|
+
body: JSON.stringify(body)
|
|
581
|
+
});
|
|
582
|
+
if (!res.ok) {
|
|
583
|
+
const data = await res.json();
|
|
584
|
+
const isQuotaExceeded = data.error === "quota_exceeded";
|
|
585
|
+
return {
|
|
586
|
+
ok: false,
|
|
587
|
+
error: String(data.error ?? `HTTP ${res.status}`),
|
|
588
|
+
quotaExceeded: isQuotaExceeded,
|
|
589
|
+
paymentType: isQuotaExceeded ? String(data.paymentType) : undefined
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
const reader = res.body.getReader();
|
|
593
|
+
const decoder = new TextDecoder;
|
|
594
|
+
let buffer = "";
|
|
595
|
+
let finalState = null;
|
|
596
|
+
let finalQuota = null;
|
|
597
|
+
while (true) {
|
|
598
|
+
const { done, value } = await reader.read();
|
|
599
|
+
if (done)
|
|
600
|
+
break;
|
|
601
|
+
buffer += decoder.decode(value, { stream: true });
|
|
602
|
+
const lines = buffer.split(`
|
|
493
603
|
`);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
604
|
+
buffer = lines.pop();
|
|
605
|
+
for (const line of lines) {
|
|
606
|
+
const trimmed = line.trim();
|
|
607
|
+
if (!trimmed || !trimmed.startsWith("data: "))
|
|
608
|
+
continue;
|
|
609
|
+
try {
|
|
610
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
611
|
+
if (data.token)
|
|
612
|
+
onToken(data.token);
|
|
613
|
+
if (data.done && data.state && data.quota) {
|
|
614
|
+
finalState = data.state;
|
|
615
|
+
finalQuota = data.quota;
|
|
616
|
+
}
|
|
617
|
+
} catch {}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (finalState && finalQuota) {
|
|
621
|
+
return { ok: true, state: finalState, quota: finalQuota };
|
|
622
|
+
}
|
|
623
|
+
return { ok: false, error: "stream ended without state" };
|
|
624
|
+
} catch (err) {
|
|
625
|
+
if (isRetryableNetworkError(err)) {
|
|
499
626
|
try {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
627
|
+
return await sendChatStreamWithCurl(apiUrl, {
|
|
628
|
+
userId,
|
|
629
|
+
name: companion.name,
|
|
630
|
+
personality: companion.personality,
|
|
631
|
+
species: companion.species,
|
|
632
|
+
mood: deriveMood(state),
|
|
633
|
+
needs: state.needs,
|
|
634
|
+
message
|
|
635
|
+
}, onToken);
|
|
636
|
+
} catch (curlErr) {
|
|
637
|
+
lastError = curlErr instanceof Error ? curlErr.message : "unknown error";
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
508
640
|
}
|
|
641
|
+
lastError = err instanceof Error ? err.message : "unknown error";
|
|
509
642
|
}
|
|
510
|
-
if (finalState && finalQuota) {
|
|
511
|
-
return { ok: true, state: finalState, quota: finalQuota };
|
|
512
|
-
}
|
|
513
|
-
return { ok: false, error: "stream ended without state" };
|
|
514
|
-
} catch (err) {
|
|
515
|
-
const msg = err instanceof Error ? err.message : "unknown error";
|
|
516
|
-
return { ok: false, error: msg };
|
|
517
643
|
}
|
|
644
|
+
return { ok: false, error: lastError };
|
|
518
645
|
}
|
|
519
646
|
function sendInteraction(userId, type) {
|
|
520
647
|
return post("/api/interaction", { userId, type });
|
|
@@ -4679,7 +4806,8 @@ function Hatch({ userId, onComplete }) {
|
|
|
4679
4806
|
|
|
4680
4807
|
// src/main.tsx
|
|
4681
4808
|
import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
|
|
4682
|
-
var
|
|
4809
|
+
var require2 = createRequire(import.meta.url);
|
|
4810
|
+
var { version: VERSION } = require2("../package.json");
|
|
4683
4811
|
async function checkUpdate() {
|
|
4684
4812
|
try {
|
|
4685
4813
|
const res = await fetch("https://registry.npmjs.org/@whaletech%2fpet/latest", {
|