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
- Requirements
- Installation
- Authentication
- Quick start
- Core concepts
- Client construction
- API reference
- Events reference
- Error handling
- Types reference
- Complete examples
- Connection notes
- Versioning & compatibility
- Development
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-sdkFrom 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-runnpm run buildinsdk/andnpm installin 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-keysREST 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 toJyotishAuthErrorby 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 anAsyncIterable<TEvent>. Usefor await…ofto consume events as they arrive. A stream throws a typedJyotishErroron transport/auth failure — wrap iteration intry/catchif you need to convert that to your own error shape. - Unary RPCs (
checkCusp,personalizedCalendar,muhurta,listPersonas) return aPromise<T>that rejects with aJyotishErroron failure.
Session lifecycle (first turn vs follow-up)
chat, deepDive, and rectifyBirthTime are session-based:
- First turn — omit
sessionId. ProvidecustomerIdand the selectedchartIdfor chat/deep-dive; provide birth data for rectification. The backend starts a session and the stream emits asession_startedevent carryingsessionId. Cache it on your side. - Follow-up turns — resend
sessionId. The backend reuses the session's already-resolved identity, so you omitcustomerId,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) andtimezoneare 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 }inHH: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
checkCuspto 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
errorevent (event.kind === 'error') is a domain-level terminal failure described byChatErrorCodeand does not throw. A thrownJyotishErrormeans 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_questionfield (number 12) was removed fromUpcomingTransitCard; usechat_questions, which carries the three open questions. The number is reserved and never reused. - 0.2.3: Upcoming Events accepts optional
language, includingvifor Vietnamese. Unsupported languages and missing translations use English. - The
sync-protoscript 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-tagsnpm 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 specsIP 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.
