npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

public-vedic-sdk

v0.2.4

Published

Node.js/TypeScript client SDK for third-party partners to call the Jyotish interpretation gRPC API.

Readme

public-vedic-sdk

A Node.js / TypeScript client SDK for the Vedic interpretation gRPC API (core_vedic.interpretation.v1.InterpretationChatService). It lets a third-party backend talk to the interpretation engine over gRPC using a partner API key — realtime chat, topic deep-dives, compatibility narratives, daily guidance, mundane (Medini Jyotish) period readings, birth-time rectification, a personalized day-by-day calendar, timing (Muhurta) windows, and two LLM-free checks (cusp ambiguity, Muhurta).

The SDK ships only the wire contract + a typed client. No knowledge-base files, prompts, dignity/Bala logic, or chart-compute logic ever leave the server (the IP-boundary invariant).


Table of contents


Overview

The interpretation engine exposes one gRPC service with ten RPCs. The SDK maps each to a typed TypeScript call:

| RPC | Kind | Returns | Purpose | |---|---|---|---| | Chat | server-stream | AsyncIterable<ChatEvent> | Free-form realtime Q&A about a chart | | DeepDive | server-stream | AsyncIterable<DeepDiveEvent> | Structured deep-dive on one topic | | CompatibilityNarrative | server-stream | AsyncIterable<DeepDiveEvent> | Couple-compatibility narrative for a saved pair | | DailyGuidance | server-stream | AsyncIterable<DeepDiveEvent> | Daily muhurta (timing) guidance narrative | | MundaneReading | server-stream | AsyncIterable<DeepDiveEvent> | Mundane (Medini Jyotish) period narrative for a date range | | RectifyBirthTime | server-stream | AsyncIterable<RectificationEvent> | Multi-turn Q&A to narrow an unknown birth time | | CheckCusp | unary | Promise<CheckCuspResponse> | LLM-free check: is a known time cusp-ambiguous? | | PersonalizedCalendar | unary | Promise<PersonalizedCalendarResponse> | LLM-free day-by-day personalized planet/timing facts | | Muhurta | unary | Promise<MuhurtaResponse> | LLM-free good/bad timing windows for a purpose on one day | | ListPersonas | unary | Promise<PersonaSummary[]> | Static catalog of agent voices |

Streaming RPCs yield typed events; unary RPCs resolve once. Every call attaches your API key as authorization: Bearer <apiKey> gRPC metadata.

Requirements

  • Node.js 18+ (LTS recommended; developed on Node 24).
  • An API key issued by an admin (see Authentication).
  • Network reachability to the engine's gRPC port (default 50051).

Installation

From npm

npm install public-vedic-sdk
# or: pnpm add public-vedic-sdk

From a local checkout

For local development, you can install straight from the folder. Build it first (dist/ is gitignored and is what main points at):

cd sdk && npm install && npm run build
# then in your app:
npm install file:../path/to/sdk

file: installs a copy. After changing SDK source, re-run npm run build in sdk/ and npm install in your app to refresh the copy.

TypeScript

Types ship in the package (dist/*.d.ts). No extra @types needed. The SDK is CommonJS ("main": "dist/index.js").

import { JyotishClient } from 'public-vedic-sdk';

Authentication

Every call is authenticated with a partner API key.

  • Format: sk_live_<48 hex chars> (e.g. sk_live_3f0a9b…).
  • Issued by an admin via the backend's developer-keys REST endpoint. The raw key is shown once at creation; only a non-reversible prefix is stored afterwards. Treat it like a password.
  • How it's sent: authorization: Bearer <apiKey> gRPC metadata — the SDK attaches this automatically on every call. You never handle metadata.
  • Validation: the backend looks the key up by its SHA-256 hash and rejects revoked/unknown keys with gRPC UNAUTHENTICATED (mapped to JyotishAuthError by the SDK).
const client = new JyotishClient(process.env.VEDIC_API_KEY!, {
  host: 'api.example.com',
  port: 50051,
});

Quick start

A first chat turn — send customerId and the exact persisted chartId to use:

import { JyotishClient } from 'public-vedic-sdk';

const client = new JyotishClient(process.env.VEDIC_API_KEY!, {
  host: 'localhost',
  port: 50051,
});

for await (const event of client.chat({
  customerId: 'cust-1',
  chartId: 'chart-1',
  message: 'Sự nghiệp của tôi thế nào?',
  mode: 'vedic',
  language: 'vi',
  personaId: 'digital-soulmate',
  responseLength: 'long',
})) {
  switch (event.kind) {
    case 'session_started':
      console.log('session:', event.event.sessionId); // cache this!
      break;
    case 'status':
      console.log(`[${event.event.label}]`); // reading_chart | synthesizing | …
      break;
    case 'text_delta':
      process.stdout.write(event.event.content); // incremental markdown
      break;
    case 'reasoning_delta':
      // optional: the model's chain-of-thought (forwarded verbatim)
      break;
    case 'error':
      console.error(event.event.code, event.event.message);
      break;
    case 'done':
      console.log('\ntokens:', event.event.tokenUsage);
      break;
  }
}

Core concepts

Streaming vs unary

  • Streaming RPCs (chat, deepDive, compatibilityNarrative, dailyGuidance, mundaneReading, rectifyBirthTime) return an AsyncIterable<TEvent>. Use for await…of to consume events as they arrive. A stream throws a typed JyotishError on transport/auth failure — wrap iteration in try/catch if you need to convert that to your own error shape.
  • Unary RPCs (checkCusp, personalizedCalendar, muhurta, listPersonas) return a Promise<T> that rejects with a JyotishError on failure.

Session lifecycle (first turn vs follow-up)

chat, deepDive, and rectifyBirthTime are session-based:

  1. First turn — omit sessionId. Provide customerId and the selected chartId for chat/deep-dive; provide birth data for rectification. The backend starts a session and the stream emits a session_started event carrying sessionId. Cache it on your side.
  2. Follow-up turns — resend sessionId. The backend reuses the session's already-resolved identity, so you omit customerId, chartId, and birth data.
// follow-up turn — sessionId only, no customerId or chartId
for await (const event of client.chat({ sessionId, message: 'Còn về tài chính?' })) {
  // ...
}

compatibilityNarrative, dailyGuidance, mundaneReading, checkCusp, personalizedCalendar, muhurta, and listPersonas are single-shot — no session.

Modes & languages

  • mode — 'vedic' (default) or 'western'. Selection is orthogonal to persona; every persona has both variants.
  • language — any language code (default 'vi'). Drives the output language of the narrative.

Customer and selected chart prerequisites (important)

chat, deepDive, dailyGuidance, personalizedCalendar, and muhurta require both a customerId and an explicit chartId. The chart ID must identify the exact persisted chart to use; the SDK does not select the latest chart. Before your first streaming call you must therefore create a customer + chart once, via the backend's partner REST API (plain HTTP, same Bearer key). The SDK intentionally covers only the gRPC streaming surface; the bootstrap is REST:

# 1) Create a customer -> { "id": "cust-…" }
curl -X POST https://api.example.com/api/v1/partner/customers \
  -H "Authorization: Bearer $VEDIC_API_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "Jane Doe", "gender": "female" }'

# 2) Create + persist their chart (computes & stores it) -> { "id": "chart-…" }
curl -X POST https://api.example.com/api/v1/partner/charts \
  -H "Authorization: Bearer $VEDIC_API_KEY" -H "Content-Type: application/json" \
  -d '{ "customerId": "cust-…", "date": "1990-05-21", "time": "14:30:00",
        "place": "Hanoi", "latitude": 21.0285, "longitude": 105.8542,
        "timezone": "Asia/Ho_Chi_Minh", "gender": "female" }'

Use the id from step 1 as customerId and the id from step 2 as chartId in SDK requests. For compatibilityNarrative, first create a pair via POST /api/v1/partner/compatibility and pass its id as compatibilityId.

Coordinates (latitude/longitude) and timezone are resolved client-side — the backend does not geocode. Send them when creating the chart.

Personas

The agent's voice. Unrecognized/empty values fall back to digital-soulmate.

| personaId | Voice | |---|---| | digital-soulmate (default) | A calm, reflective mirror — translates symbolism into relatable language | | classical-jyotishi | Professional, image-rich expert fluent in classical terminology | | practical-coach | Direct, action-oriented, plain-spoken | | poetic-storyteller | Image-rich, narrative, poetic register | | warm-friend | Casual, intimate, conversational — like a close friend | | beginner-friendly | Explains every astrological term the first time it's used | | astro-mentor | A patient teacher — explains the everyday meaning first, then names the matching astrological term |

Call listPersonas() to fetch the live catalog (labels + descriptions) for rendering a picker.

Client construction

new JyotishClient(apiKey: string, opts: JyotishClientOptions)

JyotishClientOptions

| Option | Type | Required | Description | |---|---|---|---| | host | string | yes | gRPC authority. Bare host (localhost), host:port, or a full http(s)://… URL (see Connection notes). | | port | number | no | Port when host is a bare host. Ignored for a full-URL host. | | useTls | boolean | no | Force TLS for a bare host (e.g. host:443 over TLS). Default false. | | protoPath | string | no | Override the proto path (testing/fixtures). Defaults to the bundled copy. |

The client constructs one gRPC channel at construction time and reuses it for all calls. Create one instance per app/process and share it.

API reference

All streaming methods return AsyncIterable<…Event>; the two unary methods return Promise<…>. Optional fields are omitted from the wire when undefined.


chat

Free-form realtime Q&A about a subject's chart.

client.chat(request: ChatRequest): AsyncIterable<ChatEvent>

ChatRequest

| Field | Type | Notes | |---|---|---| | sessionId? | string | Omit on the first turn; resend on follow-up turns. | | customerId? | string | Required on the first turn. Omit on follow-up. | | chartId? | string | Exact persisted chart to use; required on the first turn. Omit on follow-up. | | message | string | The user's question / follow-up. | | mode? | 'vedic' \| 'western' | Default vedic. | | language? | string | Default vi. | | responseLength? | 'short' \| 'medium' \| 'long' | Soft hint; default long. | | personaId? | PersonaId | Default digital-soulmate. | | currentTimezone? | string | IANA tz (e.g. America/New_York) — the subject's current-living tz; falls back to birth tz. First turn only. | | currentLatitude? | number | Subject's current location (not birth). Required on the first turn; persisted on the session so follow-up turns reuse it. | | currentLongitude? | number | Subject's current location (not birth). Required on the first turn; persisted on the session so follow-up turns reuse it. |

Yields ChatEvent. The terminal done event carries tokenUsage (and optional usageDetails with cost/cache/reasoning breakdown).


deepDive

A structured deep-dive on one topic; returns streamed markdown.

client.deepDive(request: DeepDiveRequest): AsyncIterable<DeepDiveEvent>

DeepDiveRequest

| Field | Type | Notes | |---|---|---| | sessionId? | string | Omit first turn; resend to continue. | | customerId? | string | Required on the first turn. Omit on follow-up. | | chartId? | string | Exact persisted chart to use; required on the first turn. Omit on follow-up. | | topic | string | One of the valid topics below. | | mode? / language? / personaId? / currentTimezone? | — | Same semantics as chat. |

Valid topics (the backend whitelists these server-side):

summary · career · finance · relationship · health · exploration · family · forecast · remedy

Yields DeepDiveEvent.


compatibilityNarrative

Streams the couple-compatibility narrative for an already-created pair. Single-shot (no session).

client.compatibilityNarrative(request: CompatibilityNarrativeRequest): AsyncIterable<DeepDiveEvent>

CompatibilityNarrativeRequest

| Field | Type | Notes | |---|---|---| | compatibilityId | string | Required — id from POST /partner/compatibility. | | mode? / language? / personaId? | — | Same as above. | | customerId? | string | Optional link to an existing customer. | | windowPreset? | '1y' \| '3y' \| '7y' | When present, folds a condensed Dasha timeline summary into the narrative context. |

Yields DeepDiveEvent.


dailyGuidance

Streams the daily muhurta (timing) guidance narrative. Single-shot.

client.dailyGuidance(request: DailyGuidanceRequest): AsyncIterable<DeepDiveEvent>

DailyGuidanceRequest

| Field | Type | Notes | |---|---|---| | customerId | string | Required — owner of the selected chart. | | chartId | string | Required — exact persisted chart to use. | | mode? / language? / personaId? / currentTimezone? | — | Same as above. |

Yields DeepDiveEvent.


mundaneReading

Streams a mundane (Medini Jyotish) period narrative for a date range — a world/collective reading, not tied to a customer. Single-shot.

client.mundaneReading(request: MundaneReadingRequest): AsyncIterable<DeepDiveEvent>

MundaneReadingRequest

| Field | Type | Notes | |---|---|---| | from | string | Required — ISO 8601, local to timezone. | | to | string | Required — ISO 8601, local to timezone. | | timezone? | string | IANA tz; server defaults to UTC. | | topic? | string | One of the topics below; server defaults to overview. | | mode? / personaId? / language? | — | Same semantics as chat (language defaults to en here). |

Valid topics (server whitelist): overview · economy · politics · conflict-disaster

Yields DeepDiveEvent.


rectifyBirthTime

Multi-turn Q&A to narrow an unknown/ambiguous birth time. Two entry cases on the first turn:

  • Case A (knownTime) — the user knows a time but it may sit on a sign cusp. Send it; the backend asks clarifying questions.
  • Case B (estimatedWindow) — the user only knows a rough window ({ start, end } in HH:MM). The backend narrows it.

knownTime and estimatedWindow are mutually exclusive. On follow-up turns, resend sessionId and put the user's answer in message.

client.rectifyBirthTime(request: RectifyBirthTimeRequest): AsyncIterable<RectificationEvent>

RectifyBirthTimeRequest

| Field | Type | Notes | |---|---|---| | sessionId? | string | Omit first turn; resend to continue. | | date / place / gender | string | First-turn identifying fields (omitted on follow-up). | | latitude? / longitude? / timezone? | — | Coordinates + tz (client-resolved). | | sexualOrientation? | string | Only meaningful when gender === 'other'. | | mode? / language? | — | As above. | | knownTime? | string | Case A — HH:MM:SS. Mutually exclusive with estimatedWindow. | | estimatedWindow? | { start: string; end: string } | Case B — HH:MM each. Required when knownTime is empty. | | message? | string | The user's answer on a follow-up turn. | | customerId? | string | Acting customer, for admin-side attribution. First turn only (persisted on the session). |

Yields RectificationEvent. The terminal result event carries the resolved time (finalTime), confidence tier, rationale, and mode-specific ascendant detail (see RectificationResultEvent).

Before starting a (paid) rectification session, use the LLM-free checkCusp to decide whether rectification is even needed.


checkCusp

Cheap, LLM-free check: is a known birth time cusp-ambiguous? Never starts a session or costs a model turn. Use it to decide whether to offer rectification.

client.checkCusp(request: CheckCuspRequest): Promise<CheckCuspResponse>

CheckCuspRequest

| Field | Type | Notes | |---|---|---| | date / place / gender | string | Required identifying fields. | | latitude? / longitude? / timezone? | — | Coordinates + tz. | | sexualOrientation? | string | Only when gender === 'other'. | | mode? | 'vedic' \| 'western' | Default vedic. | | knownTime | string | Required — HH:MM:SS. |

CheckCuspResponse

| Field | Type | Notes | |---|---|---| | needsQa | boolean | true = ambiguous, rectification recommended. | | ascSign | string | Set when needsQa === false (resolved). | | confidenceTier | ConfidenceTier \| '' | "Very High" when resolved; "" when ambiguous. |


personalizedCalendar

Cheap, LLM-free unary call: one month (or any range) of day-by-day personalized planet/timing facts for a customer's chart — Panchanga, Tara Bala / Chandra Bala, transits, natal activations, transit-transit aspects, and running Dasha lords. No LLM narrative; use it to power your own calendar UI or to feed a subsequent chat/deepDive call.

client.personalizedCalendar(request: PersonalizedCalendarRequest): Promise<PersonalizedCalendarResponse>

PersonalizedCalendarRequest

| Field | Type | Notes | |---|---|---| | customerId | string | Required — owner of the selected chart. | | chartId | string | Required — exact persisted chart to use. | | from / to | string | Required — YYYY-MM-DD, inclusive range. | | mode? | 'vedic' \| 'western' | Default vedic. | | language? | string | Default vi. | | currentTimezone? | string | Falls back to the subject's birth timezone. |

PersonalizedCalendarResponse — { days: PersonalizedCalendarDay[] }, one entry per date in range. See PersonalizedCalendarDay for the full per-day shape (Panchanga, Tara/Chandra Bala, transits, natal activations, transit aspects, Dasha).


upcomingTransits

Cheap, LLM-free unary call: one page of the chart's upcoming transit timeline — planet ingresses (kind: 'event') and transit-transit aspects (kind: 'aspect') — each card carrying the astrological reasoning behind it. narrative is template-rendered, not LLM-generated; hand a card to chat/deepDive with one of its chatQuestions when the user wants to go deeper.

client.upcomingTransits(request: UpcomingTransitsRequest): Promise<UpcomingTransitsResponse>

UpcomingTransitsRequest

| Field | Type | Notes | |---|---|---| | customerId | string | Required — owner of the selected chart. | | chartId | string | Required — exact persisted chart to use. | | horizonDays | 30 \| 60 \| 90 \| 180 | Required — requested local-calendar horizon. | | mode | 'vedic' \| 'western' | Required — western swaps nakshatra facts for lunar-mansion facts. | | currentTimezone? | string | Falls back to the chart timezone. | | lookbackDays? | number | Default 14; valid 0–30. Set 0 to exclude the recent past. | | language? | string | Default en. Use vi for Vietnamese; unsupported languages or missing translations fall back to English. | | cursor? | string | Empty on the first page; pass the previous page's nextCursor to continue. |

UpcomingTransitsResponse

| Field | Type | Notes | |---|---|---| | cards | UpcomingTransitCard[] | This page of the timeline. | | nextCursor | string | Empty when the timeline is complete. | | asOfUtc | string | Query anchor; frozen across continuation requests. |

Each UpcomingTransitCard has kind, eventType, sortAtUtc / sortAtLocal, timing (just_happened · ongoing · future), title, narrative, highlighted, resonance (none · indirect · direct), chatQuestions (three open questions about the card), a milestones object (entryAt* / exactAt* / exitAt*, UTC + local, earlierMilestoneMissing), and a why object: the transit facts, dashaPeriods, resonance matches (+ primaryMatch), and houseThemes. astroNote holds technical prose (planet names, numbered house phrase, period sentence) for a detail panel — it is never part of narrative.

The cursor is opaque and request-bound: continuing a page requires the same customerId, chartId, mode, horizonDays and currentTimezone as the call that produced it. Anything else — or a malformed cursor — rejects with JyotishInvalidArgumentError.

Language affects title, narrative, chatQuestions, astroNote and why.houseThemes; chart facts, IDs, ordering and milestones stay unchanged. Pass the requested language on every continuation request and when opening chat with a suggested question. Regional codes such as vi-VN resolve to the supported base language. Additional language codes can be passed without an SDK update; the backend owns translation availability and English fallback.


muhurta

Cheap, LLM-free unary call: one day's good/bad timing windows for a purpose, ranked by Tara Bala / Chandra Bala / purpose nature-group match.

client.muhurta(request: MuhurtaRequest): Promise<MuhurtaResponse>

MuhurtaRequest

| Field | Type | Notes | |---|---|---| | customerId | string | Required — owner of the selected chart. | | chartId | string | Required — exact persisted chart to use. | | date | string | Required — YYYY-MM-DD, single day. | | purpose | string | Required — one of: Weddings, Construction, Grand opening, Long travel, Litigation, Medical treatment, Study, Business, Contract, Finance, Property, Other. | | currentLatitude / currentLongitude | number | Required — current location (not birth); used for sunrise/sunset. | | mode? | 'vedic' \| 'western' | Default vedic. | | language? | string | Default vi. | | currentTimezone? | string | Falls back to the subject's birth timezone. |

MuhurtaResponse

| Field | Type | Notes | |---|---|---| | available | boolean | false when no window in the day qualifies. | | windows | MuhurtaWindow[] | Merged good/bad time windows for the day. | | sunriseTime / sunsetTime | string | HH:mm, local to currentTimezone. |

Each MuhurtaWindow has startTime/endTime (HH:mm), a quality tier (HighlySuitable · Suitable · Mixed · Caution · NotRecommended), and supportiveReasons / cautionReasons / infoTags string arrays explaining the ranking.


listPersonas

Unary — resolves once with the static persona catalog (for rendering a voice picker). Rejects on gRPC failure.

client.listPersonas(): Promise<PersonaSummary[]>
interface PersonaSummary {
  id: string;        // e.g. "digital-soulmate"
  label: string;     // e.g. "Digital Soulmate"
  description: string;
}

Events reference

All streaming events are discriminated unions keyed on kind. Consume with a switch on event.kind; each variant's payload lives under event.event.

ChatEvent & DeepDiveEvent (identical shape)

| kind | event payload | Meaning | |---|---|---| | session_started | { sessionId: string } | Emitted once on a new session — cache sessionId. | | status | { label: ChatStatusLabel } | Coarse progress marker. | | text_delta | { content: string } | Incremental answer text (markdown). Concatenate in order. | | reasoning_delta | { content: string } | Model chain-of-thought, forwarded verbatim (optional to display). | | error | { code: ChatErrorCode; message: string } | Terminal failure. | | done | DoneEvent | Terminal success — carries token usage. |

ChatStatusLabel values: reading_chart, preparing_context, synthesizing, completed, model_thinking, looking_up_details, plus tool-specific checking_* labels (open-ended).

ChatErrorCode values: INVALID_ARGUMENT, SESSION_NOT_FOUND, MODEL_UNAVAILABLE, MODEL_FAILED, INTERNAL.

DoneEvent

interface DoneEvent {
  sessionId: string;
  tokenUsage: { prompt: number; completion: number; total: number };
  usageDetails?: {                 // raw provider usage breakdown (optional)
    promptTokens: number;
    completionTokens: number;
    totalTokens: number;
    cost: number;
    isByok: boolean;               // bring-your-own-key?
    cachedTokens: number;
    reasoningTokens: number;
  };
}

RectificationEvent

Same as the chat union except the terminal success is result (not done):

| kind | event payload | Meaning | |---|---|---| | session_started / status / text_delta / reasoning_delta / error | — | As above (text_delta is the next clarifying question). | | result | RectificationResultEvent | Terminal — the resolved birth time. |

Error handling

Stream failures throw and unary failures reject with a typed subclass of JyotishError (all carry .code: number — the gRPC status — and .message: string).

JyotishError
├── JyotishAuthError              // UNAUTHENTICATED  — bad/revoked API key
├── JyotishInvalidArgumentError   // INVALID_ARGUMENT — bad request fields
├── JyotishSessionNotFoundError   // NOT_FOUND        — unknown sessionId
├── JyotishModelError             // UNAVAILABLE      — model/provider down
└── JyotishInternalError          // anything else (default INTERNAL)

| gRPC status | Error class | |---|---| | UNAUTHENTICATED (16) | JyotishAuthError | | INVALID_ARGUMENT (3) | JyotishInvalidArgumentError | | NOT_FOUND (5) | JyotishSessionNotFoundError | | UNAVAILABLE (14) | JyotishModelError | | anything else | JyotishInternalError |

import { JyotishAuthError, JyotishModelError } from 'public-vedic-sdk';

try {
  for await (const event of client.chat({ customerId, chartId, message })) {
    // ...
  }
} catch (err) {
  if (err instanceof JyotishAuthError) {
    // key invalid/revoked — stop retrying, alert an operator
  } else if (err instanceof JyotishModelError) {
    // transient — model/provider unavailable, safe to retry with backoff
  } else {
    // JyotishInvalidArgumentError | JyotishSessionNotFoundError | JyotishInternalError
  }
}

Note the two failure channels: an in-stream error event (event.kind === 'error') is a domain-level terminal failure described by ChatErrorCode and does not throw. A thrown JyotishError means the stream itself broke (transport/auth). Handle both.

Types reference

All public types are exported from the package root.

// Enums / unions
type ChatMode            = 'vedic' | 'western';
type ChatResponseLength  = 'short' | 'medium' | 'long';
type PersonaId           = 'digital-soulmate' | 'classical-jyotishi' | 'practical-coach'
                         | 'poetic-storyteller' | 'warm-friend' | 'beginner-friendly' | 'astro-mentor';
type ConfidenceTier      = 'Very High' | 'High' | 'Moderate' | 'Low';
type ChatStatusLabel     = 'reading_chart' | 'preparing_context' | 'synthesizing'
                         | 'completed' | 'model_thinking' | 'looking_up_details' | (string & {});
type ChatErrorCode       = 'INVALID_ARGUMENT' | 'SESSION_NOT_FOUND'
                         | 'MODEL_UNAVAILABLE' | 'MODEL_FAILED' | 'INTERNAL';
type MuhurtaQuality      = 'HighlySuitable' | 'Suitable' | 'Mixed' | 'Caution' | 'NotRecommended';
type UpcomingTransitKind = 'event' | 'aspect';
type UpcomingTransitResonance = 'none' | 'indirect' | 'direct';
type UpcomingTransitDashaTier = 'maha' | 'antar' | 'praty';

// Requests
interface ChatRequest { /* see API reference */ }
interface DeepDiveRequest { /* … */ }
interface CompatibilityNarrativeRequest { /* … */ }
interface DailyGuidanceRequest { /* … */ }
interface MundaneReadingRequest { /* … */ }
interface RectifyBirthTimeRequest { /* … */ }
interface CheckCuspRequest { /* … */ }
interface PersonalizedCalendarRequest { /* … */ }
interface UpcomingTransitsRequest { /* … */ }
interface MuhurtaRequest { /* … */ }

// Responses / events
interface CheckCuspResponse { needsQa: boolean; ascSign: string; confidenceTier: ConfidenceTier | ''; }
interface PersonaSummary { id: string; label: string; description: string; }
interface TimeWindow { start: string; end: string; }   // HH:MM local

interface RectificationResultEvent {
  ascSign: string;
  narrowedWindow?: TimeWindow;
  confidenceTier: ConfidenceTier;
  remainingCandidateCount: number;
  rationale: string;
  truncated: boolean;
  finalTime: string;                       // "HH:MM:00" local — the resolved pick
  ascendantPada?: number;                  // vedic: winning ASC nakshatra pada (1-4)
  ascendantQuarter?: number;               // western: winning ASC lunar-mansion quarter (1-4)
  ascendantNakshatra?: string;             // vedic: resolved nakshatra name
  ascendantLunarMansion?: string;          // western: resolved lunar-mansion name
  ascendantResolutionPhase?: 'sign' | 'station' | 'pada'; // deepest tier resolved
}

// PersonalizedCalendar — one entry in PersonalizedCalendarResponse.days
interface PersonalizedCalendarDay {
  date: string;                            // YYYY-MM-DD
  panchanga: {                             // classical 5-limb Panchanga
    tithiName: string; tithiIndex: number; vara: string;
    nakshatra: string; nakshatraLord: string; yoga: string; karana: string;
  };
  personal: {                              // Tara Bala + Chandra Bala synthesis
    taraBala: { taraName: string; nature: 'favorable' | 'unfavorable' | 'neutral' };
    chandraBala: { houseFromNatalMoon: number; favorable: boolean };
    supportive: string[]; cautions: string[]; explanation: string;
  };
  transits: Array<{                        // all 9 canonical planets, always present
    planet: string; sign: string; degree: string; nakshatra: string; nakshatraLord: string;
    houseFromLagna: number; houseType: string; retrograde: boolean;
  }>;
  natalActivations: Array<{                // transiting Drishti onto natal houses
    transitPlanet: string; targetHouse: number; targetPlanet: string;
    targetIsLagna: boolean; aspectStrength: 'weak' | 'medium' | 'standard' | 'strong';
  }>;
  transitAspects: Array<{                  // transit-transit Drishti, no K4 amplification
    planetA: string; planetB: string;
    phase: 'entry' | 'applying' | 'exact' | 'separating' | 'exit'; orb: number; strength: string;
  }>;
  dasha: { mahadashaLord: string; antardashaLord: string; pratyantardashaLord: string };
}

interface MuhurtaWindow {
  startTime: string; endTime: string;      // HH:mm, local to currentTimezone
  quality: MuhurtaQuality;
  supportiveReasons: string[]; cautionReasons: string[]; infoTags: string[];
}
interface MuhurtaResponse {
  available: boolean;
  windows: MuhurtaWindow[];
  sunriseTime: string; sunsetTime: string; // HH:mm, local to currentTimezone
}

// UpcomingTransits — one card in UpcomingTransitsResponse.cards
interface UpcomingTransitCard {
  id: string;
  kind: UpcomingTransitKind;
  eventType: string;
  sortAtUtc: string; sortAtLocal: string;
  title: string; narrative: string; highlighted: boolean;
  resonance: UpcomingTransitResonance;
  why: {
    transit: Array<{ planet: string; sign: string; house: number; source: string /* event | aspect */ }>;
    dashaPeriods: Array<{
      tier: UpcomingTransitDashaTier; lord: string;
      governedHouses: number[]; occupiedHouse?: number; activatedHouses: number[];
    }>;
    matches: Array<{
      tier: UpcomingTransitDashaTier; lord: string;
      relation: Exclude<UpcomingTransitResonance, 'none'>;
      planet: string; sign: string; house: number;
      governedHouses: number[]; occupiedHouse?: number;
    }>;
    primaryMatch?: /* same shape as matches[number] */;
    houseThemes: Array<{ house: number; theme: string }>;
    mode: ChatMode;
  };
  milestones: {
    entryAtUtc: string; entryAtLocal: string; // '' when the milestone does not apply
    exactAtUtc: string; exactAtLocal: string;
    exitAtUtc: string; exitAtLocal: string;
    earlierMilestoneMissing: boolean;
  };
  chatQuestions: string[];                 // three open questions about the card
  astroNote: string;                       // technical prose for a detail panel
}
interface UpcomingTransitsResponse {
  cards: UpcomingTransitCard[];
  nextCursor: string;                      // '' when the timeline is complete
  asOfUtc: string;                         // frozen across continuation requests
}

type ChatEvent           = /* discriminated union — see Events reference */;
type DeepDiveEvent       = /* same shape as ChatEvent */;
type RectificationEvent  = /* union with a `result` terminal variant */;

Complete examples

Multi-turn chat

const client = new JyotishClient(API_KEY, { host: 'localhost', port: 50051 });

async function ask(customerId: string, chartId: string, sessionId: string | undefined, message: string) {
  let nextSessionId = sessionId;
  for await (const event of client.chat(
    sessionId ? { sessionId, message } : { customerId, chartId, message, mode: 'vedic', language: 'vi' },
  )) {
    if (event.kind === 'session_started') nextSessionId = event.event.sessionId;
    if (event.kind === 'text_delta') process.stdout.write(event.event.content);
    if (event.kind === 'error') throw new Error(`${event.event.code}: ${event.event.message}`);
  }
  return nextSessionId;
}

let sessionId = await ask('cust-1', 'chart-1', undefined, 'Tổng quan lá số của tôi?');
sessionId = await ask('cust-1', 'chart-1', sessionId, 'Còn về tài chính?');

Topic deep-dive

for await (const event of client.deepDive({
  customerId: 'cust-1',
  chartId: 'chart-1',
  topic: 'career',
  mode: 'western',
  personaId: 'practical-coach',
})) {
  if (event.kind === 'text_delta') process.stdout.write(event.event.content);
}

Rectification with a cusp pre-check

// 1) Free check — is the known time even ambiguous?
const cusp = await client.checkCusp({
  date: '1990-05-21', place: 'Hanoi',
  latitude: 21.0285, longitude: 105.8542, timezone: 'Asia/Ho_Chi_Minh',
  gender: 'female', mode: 'vedic', knownTime: '14:30:00',
});
if (!cusp.needsQa) {
  console.log(`No rectification needed (asc ${cusp.ascSign}, ${cusp.confidenceTier}).`);
} else {
  // 2) Start a rectification session with the known time (Case A)
  for await (const event of client.rectifyBirthTime({
    date: '1990-05-21', place: 'Hanoi',
    latitude: 21.0285, longitude: 105.8542, timezone: 'Asia/Ho_Chi_Minh',
    gender: 'female', mode: 'vedic', language: 'vi', knownTime: '14:30:00',
  })) {
    if (event.kind === 'result') {
      console.log(`Resolved: ${event.event.finalTime} (${event.event.confidenceTier})`);
      console.log(event.event.rationale);
    }
  }
}

Daily guidance

for await (const event of client.dailyGuidance({
  customerId: 'cust-1',
  chartId: 'chart-1',
  mode: 'vedic',
  language: 'vi',
  currentTimezone: 'America/New_York',
})) {
  if (event.kind === 'text_delta') process.stdout.write(event.event.content);
}

Mundane (Medini Jyotish) reading

for await (const event of client.mundaneReading({
  from: '2026-07-01T00:00:00Z',
  to: '2026-07-31T00:00:00Z',
  topic: 'economy',
  mode: 'vedic',
})) {
  if (event.kind === 'text_delta') process.stdout.write(event.event.content);
}

Personalized calendar + Muhurta (LLM-free)

// One month of day-by-day facts, for your own calendar UI
const calendar = await client.personalizedCalendar({
  customerId: 'cust-1',
  chartId: 'chart-1',
  from: '2026-08-01',
  to: '2026-08-31',
  currentTimezone: 'Asia/Ho_Chi_Minh',
});
console.log(calendar.days[0].panchanga.tithiName);

// Best time windows today for a specific purpose
const { windows } = await client.muhurta({
  customerId: 'cust-1',
  chartId: 'chart-1',
  date: '2026-08-12',
  purpose: 'Business',
  currentLatitude: 10.7769,
  currentLongitude: 106.7009,
  currentTimezone: 'Asia/Ho_Chi_Minh',
});
for (const w of windows) {
  console.log(`${w.startTime}-${w.endTime}: ${w.quality}`);
}

Upcoming transit timeline (LLM-free, paginated)

// Walk the whole horizon, one page at a time. Keep the cursor-bound request
// fields identical across pages — the backend rejects a mismatched cursor.
let cursor: string | undefined;
do {
  const page = await client.upcomingTransits({
    customerId: 'cust-1',
    chartId: 'chart-1',
    horizonDays: 90,
    mode: 'vedic',
    language: 'vi',
    currentTimezone: 'Asia/Ho_Chi_Minh',
    cursor,
  });
  for (const card of page.cards) {
    console.log(`${card.sortAtLocal} · ${card.title} — ${card.narrative}`);
  }
  cursor = page.nextCursor || undefined;
} while (cursor);

Connection notes

The constructor's host accepts three forms; the SDK resolves the gRPC target and TLS credentials accordingly:

| host form | Example | Target | Credentials | |---|---|---|---| | Bare host + port | localhost / 50051 | localhost:50051 | insecure (useTls opts into SSL) | | host:port (no port) | api.example.com:443 | as-is | insecure unless useTls | | Full https:// URL | https://tunnel.example.com | tunnel.example.com:443 | SSL (TLS terminated at the edge) | | Full http:// URL | http://local.box:50051 | local.box:50051 | insecure |

A full URL is useful for tunnels (e.g. devtunnels / cloudflare) where the gRPC authority is a public hostname. The scheme/path are stripped to a host:port authority — grpc-js would otherwise mis-parse a raw https://… string.

// Behind a TLS tunnel
new JyotishClient(API_KEY, { host: 'https://vedic-tunnel.example.com' });

// Bare host, forced TLS
new JyotishClient(API_KEY, { host: 'api.example.com', port: 443, useTls: true });

Versioning & compatibility

  • The wire contract is interpretation-chat.proto (package core_vedic.interpretation.v1). Proto field numbers are stable once published — never reused or renumbered.
  • Additive proto changes (new optional fields, new RPCs) are non-breaking; the SDK is regenerated/extended to match. Breaking changes bump the package major version.
  • 0.2.0: the singular chat_question field (number 12) was removed from UpcomingTransitCard; use chat_questions, which carries the three open questions. The number is reserved and never reused.
  • 0.2.3: Upcoming Events accepts optional language, including vi for Vietnamese. Unsupported languages and missing translations use English.
  • The sync-proto script copies the backend proto when the source checkout is available (see Development). In this standalone repository, verify the bundled proto against the backend source before a release; the local sync test only checks the error when that source is unavailable.

Development

npm install
npm test             # 35 tests, including a pack + install + TypeScript consumer smoke test
npm run build        # tsc -> dist/

npm run sync-proto is a maintainer-only command. It requires a checkout that places this SDK beside the backend proto source, then copies the source proto into proto/.

Releasing a new version

Publish the package to npm after bumping its version and validating the packed contents:

# 1. bump the version field
npm version patch   # or: minor / major — updates package.json, commits, and tags

# 2. validate the release artifact
npm run build
npm test
npm pack --dry-run

# 3. publish and push the release commit/tag
npm publish --access public
git push origin main --follow-tags

npm version <bump> creates the tag as vX.Y.Z automatically. If you tag manually instead, keep the tag name in sync with package.json's version field.

Repo layout

sdk/
├── proto/interpretation-chat.proto   # wire contract (synced from backend)
├── src/
│   ├── client.ts                     # JyotishClient — all 10 RPCs
│   ├── types.ts                      # public TS shapes mirroring the proto
│   ├── errors.ts                     # JyotishError hierarchy + mapGrpcError
│   ├── grpc-loader.ts                # runtime proto-loader wrapper
│   └── index.ts                      # barrel exports
├── scripts/sync-proto.ts             # drift guard (backend -> sdk)
└── test/                             # mock gRPC server + client/pack/ip-boundary specs

IP boundary — test/ip-boundary.spec.ts fails if any src/ file references backend business logic (knowledge_base, modules/charts, etc.). This package must remain a pure client.


License

MIT