@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/main.js +208 -80
  3. 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.ai` by default. Override with:
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
- var API_URL = process.env["WHALE_API_URL"] ?? "https://api.pet.whaletech.app";
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
- async function post(path2, body) {
418
- try {
419
- if (_token)
420
- body.token = _token;
421
- const res = await fetch(`${API_URL}${path2}`, {
422
- method: "POST",
423
- headers: { "Content-Type": "application/json" },
424
- body: JSON.stringify(body)
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
- const data = await res.json();
427
- if (!res.ok) {
428
- const isQuotaExceeded = data.error === "quota_exceeded";
429
- return {
430
- ok: false,
431
- error: String(data.error ?? `HTTP ${res.status}`),
432
- quotaExceeded: isQuotaExceeded,
433
- paymentType: isQuotaExceeded ? String(data.paymentType) : undefined
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
- try {
456
- const body = {
457
- userId,
458
- name: companion.name,
459
- personality: companion.personality,
460
- species: companion.species,
461
- mood: deriveMood(state),
462
- needs: state.needs,
463
- message
464
- };
465
- if (_token)
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
- const reader = res.body.getReader();
483
- const decoder = new TextDecoder;
484
- let buffer = "";
485
- let finalState = null;
486
- let finalQuota = null;
487
- while (true) {
488
- const { done, value } = await reader.read();
489
- if (done)
490
- break;
491
- buffer += decoder.decode(value, { stream: true });
492
- const lines = buffer.split(`
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
- buffer = lines.pop();
495
- for (const line of lines) {
496
- const trimmed = line.trim();
497
- if (!trimmed || !trimmed.startsWith("data: "))
498
- continue;
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
- const data = JSON.parse(trimmed.slice(6));
501
- if (data.token)
502
- onToken(data.token);
503
- if (data.done && data.state && data.quota) {
504
- finalState = data.state;
505
- finalQuota = data.quota;
506
- }
507
- } catch {}
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 VERSION = "0.2.0";
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", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whaletech/pet",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Terminal pet companion — hatch, feed, and chat with your ASCII whale",
5
5
  "type": "module",
6
6
  "license": "MIT",