jpdcl 1.0.0
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/.env.example +15 -0
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/catalog.d.ts +141 -0
- package/dist/catalog.js +261 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +395 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +13 -0
- package/dist/credentials.js +100 -0
- package/dist/credentials.js.map +1 -0
- package/dist/crypto.d.ts +3 -0
- package/dist/crypto.js +21 -0
- package/dist/crypto.js.map +1 -0
- package/dist/dates.d.ts +5 -0
- package/dist/dates.js +20 -0
- package/dist/dates.js.map +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +23 -0
- package/dist/errors.js.map +1 -0
- package/dist/http-handler.d.ts +1 -0
- package/dist/http-handler.js +46 -0
- package/dist/http-handler.js.map +1 -0
- package/dist/http.d.ts +9 -0
- package/dist/http.js +43 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/launcher.d.ts +2 -0
- package/dist/launcher.js +11 -0
- package/dist/launcher.js.map +1 -0
- package/dist/ledger-client.d.ts +112 -0
- package/dist/ledger-client.js +221 -0
- package/dist/ledger-client.js.map +1 -0
- package/dist/main-client.d.ts +25 -0
- package/dist/main-client.js +221 -0
- package/dist/main-client.js.map +1 -0
- package/dist/mcp-stdio.d.ts +2 -0
- package/dist/mcp-stdio.js +6 -0
- package/dist/mcp-stdio.js.map +1 -0
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.js +517 -0
- package/dist/mcp.js.map +1 -0
- package/dist/runtime.d.ts +27 -0
- package/dist/runtime.js +316 -0
- package/dist/runtime.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +221 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +30 -0
- package/dist/session.js.map +1 -0
- package/dist/smart-client.d.ts +50 -0
- package/dist/smart-client.js +238 -0
- package/dist/smart-client.js.map +1 -0
- package/dist/tariff.d.ts +121 -0
- package/dist/tariff.js +124 -0
- package/dist/tariff.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/worker.d.ts +5 -0
- package/dist/worker.js +3 -0
- package/dist/worker.js.map +1 -0
- package/openapi.yaml +284 -0
- package/package.json +89 -0
- package/scripts/audit-http.ts +36 -0
- package/scripts/audit-launcher.mjs +28 -0
- package/scripts/audit-ledger-portal.ts +23 -0
- package/scripts/audit-mcp.mjs +140 -0
- package/scripts/audit-smart-portal.mjs +63 -0
- package/scripts/build.mjs +19 -0
- package/scripts/publish-smithery.mjs +47 -0
- package/src/catalog.ts +291 -0
- package/src/cli.ts +397 -0
- package/src/config.ts +15 -0
- package/src/credentials.ts +114 -0
- package/src/crypto.ts +21 -0
- package/src/dates.ts +19 -0
- package/src/errors.ts +26 -0
- package/src/http-handler.ts +48 -0
- package/src/http.ts +49 -0
- package/src/index.ts +13 -0
- package/src/launcher.ts +9 -0
- package/src/ledger-client.ts +258 -0
- package/src/main-client.ts +230 -0
- package/src/mcp-stdio.ts +6 -0
- package/src/mcp.ts +546 -0
- package/src/runtime.ts +329 -0
- package/src/server.ts +218 -0
- package/src/session.ts +29 -0
- package/src/smart-client.ts +249 -0
- package/src/tariff.ts +148 -0
- package/src/toolkit.test.ts +136 -0
- package/src/types.ts +51 -0
- package/src/worker.ts +3 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +16 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { input, password as passwordPrompt } from "@inquirer/prompts";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import { endpointCatalog, isEndpointName, listEndpoints } from "./catalog.js";
|
|
6
|
+
import { assertDateRange } from "./dates.js";
|
|
7
|
+
import { credentialStatus, deleteEnvCredentials, storeEnvCredentials } from "./credentials.js";
|
|
8
|
+
import { JpdclError } from "./errors.js";
|
|
9
|
+
import { clearSession } from "./session.js";
|
|
10
|
+
import { JPDCL_TARIFF_ORDER_2025_26, type DomesticTariffInput } from "./tariff.js";
|
|
11
|
+
import { JpdclRuntime } from "./runtime.js";
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
program
|
|
15
|
+
.name("jpdcl")
|
|
16
|
+
.description("API-only CLI for Jammu Power Distribution Corporation Limited (JPDCL) consumer and smart-meter services")
|
|
17
|
+
.version("1.0.0")
|
|
18
|
+
.option("--compact", "print compact JSON");
|
|
19
|
+
|
|
20
|
+
const print = (value: unknown) => {
|
|
21
|
+
const compact = program.opts<{ compact?: boolean }>().compact;
|
|
22
|
+
process.stdout.write(`${JSON.stringify(value, null, compact ? 0 : 2)}\n`);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const auth = program.command("auth").description("Manage API authentication");
|
|
26
|
+
auth
|
|
27
|
+
.command("login")
|
|
28
|
+
.description("Log in and save a session; optionally save credentials to the private project .env")
|
|
29
|
+
.option("--login-id <id>", "mobile number or email")
|
|
30
|
+
.option("--save-env", "store credentials in .env for automatic MCP/CLI re-login")
|
|
31
|
+
.action(async (options: { loginId?: string; saveEnv?: boolean }) => {
|
|
32
|
+
const loginId = options.loginId ?? process.env.JPDCL_LOGIN_ID ?? await input({ message: "JPDCL mobile/email:" });
|
|
33
|
+
const secret = process.env.JPDCL_PASSWORD ?? await passwordPrompt({ message: "JPDCL password:", mask: "*" });
|
|
34
|
+
const runtime = await JpdclRuntime.create();
|
|
35
|
+
const result = await runtime.login(loginId, secret);
|
|
36
|
+
const envFile = options.saveEnv ? await storeEnvCredentials(loginId, secret) : undefined;
|
|
37
|
+
print({
|
|
38
|
+
status: result.status,
|
|
39
|
+
message: result.message ?? "Login successful",
|
|
40
|
+
sessionSaved: true,
|
|
41
|
+
automaticRelogin: Boolean(options.saveEnv || (process.env.JPDCL_LOGIN_ID && process.env.JPDCL_PASSWORD)),
|
|
42
|
+
credentialStorage: options.saveEnv ? "private .env" : "not stored",
|
|
43
|
+
envFile,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
auth.command("status").action(async () => {
|
|
47
|
+
const runtime = await JpdclRuntime.create();
|
|
48
|
+
const session = runtime.main.currentSession;
|
|
49
|
+
const automatic = await credentialStatus();
|
|
50
|
+
print(session ? {
|
|
51
|
+
authenticated: true,
|
|
52
|
+
loginId: session.loginId,
|
|
53
|
+
primaryAccountId: session.primaryAccountId,
|
|
54
|
+
smartAuthenticated: Boolean(session.smart?.token),
|
|
55
|
+
smartExpiresAt: session.smart?.expiresAt,
|
|
56
|
+
automaticRelogin: automatic.automaticRelogin,
|
|
57
|
+
credentialSource: automatic.source,
|
|
58
|
+
credentialFile: automatic.envFile,
|
|
59
|
+
updatedAt: session.updatedAt,
|
|
60
|
+
} : { authenticated: false });
|
|
61
|
+
});
|
|
62
|
+
auth.command("logout").option("--forget", "also remove JPDCL credentials from .env").action(async (options: { forget?: boolean }) => {
|
|
63
|
+
const runtime = await JpdclRuntime.create();
|
|
64
|
+
const credentialsRemoved = options.forget ? await deleteEnvCredentials() : false;
|
|
65
|
+
await clearSession();
|
|
66
|
+
print({ authenticated: false, sessionRemoved: true, credentialsRemoved, automaticReloginMayRemain: !options.forget });
|
|
67
|
+
});
|
|
68
|
+
auth.command("forget-credentials").description("Remove JPDCL credentials from the private project .env")
|
|
69
|
+
.action(async () => {
|
|
70
|
+
print({ removed: await deleteEnvCredentials(), sessionKept: true });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.command("catalog")
|
|
75
|
+
.description("List every mapped JPDCL and smart-meter endpoint")
|
|
76
|
+
.option("--portal <portal>", "main, smart, or ledger")
|
|
77
|
+
.action((options: { portal?: string }) => {
|
|
78
|
+
if (options.portal && !["main", "smart", "ledger"].includes(options.portal)) {
|
|
79
|
+
throw new JpdclError("--portal must be main, smart, or ledger", 400);
|
|
80
|
+
}
|
|
81
|
+
print(listEndpoints(options.portal as "main" | "smart" | "ledger" | undefined));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const account = program.command("account").description("Consumer account information");
|
|
85
|
+
account.command("info").option("--account <id>").action(async ({ account }: { account?: string }) => {
|
|
86
|
+
const runtime = await readyRuntime();
|
|
87
|
+
print(await runtime.main.customerInfo(account));
|
|
88
|
+
await runtime.persist();
|
|
89
|
+
});
|
|
90
|
+
account.command("digest").option("--account <id>").action(async ({ account }: { account?: string }) => {
|
|
91
|
+
const runtime = await readyRuntime();
|
|
92
|
+
print(await runtime.main.digest(account));
|
|
93
|
+
await runtime.persist();
|
|
94
|
+
});
|
|
95
|
+
account.command("linked").action(async () => {
|
|
96
|
+
const runtime = await readyRuntime();
|
|
97
|
+
print(await runtime.main.linkedAccounts());
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
program.command("snapshot").description("Best AI-ready factual snapshot across JPDCL and smart-meter APIs")
|
|
101
|
+
.option("--account <id>")
|
|
102
|
+
.action(async (o: { account?: string }) => {
|
|
103
|
+
const runtime = await readyRuntime();
|
|
104
|
+
print(await runtime.aiSnapshot(o.account));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const tariff = program.command("tariff").description("Official JPDCL tariff rates and deterministic charge estimates");
|
|
108
|
+
tariff.command("rates").description("Show the encoded FY 2025-26 domestic tariff and source pages")
|
|
109
|
+
.action(() => print(JPDCL_TARIFF_ORDER_2025_26));
|
|
110
|
+
tariff.command("estimate").description("Calculate domestic charges; current smart-meter month and account load are automatic by default")
|
|
111
|
+
.option("--account <id>")
|
|
112
|
+
.option("--units <kwh>", "override automatically selected current-period meter consumption")
|
|
113
|
+
.option("--load <kw>", "override portal sanctioned load")
|
|
114
|
+
.option("--prepaid", "apply the official 2% prepaid energy rebate")
|
|
115
|
+
.option("--solar-water-heater", "apply the Rs.150 verified solar-water-heater rebate")
|
|
116
|
+
.option("--electricity-duty <amount>", "explicit duty amount; never guessed")
|
|
117
|
+
.option("--other-charges <amount>", "explicit adjustments or other charges")
|
|
118
|
+
.option("--unpaid-principal <amount>", "principal subject to late-payment surcharge")
|
|
119
|
+
.option("--late-months <months>", "months late at 1.5% per month")
|
|
120
|
+
.action(async (o: {
|
|
121
|
+
account?: string; units?: string; load?: string; prepaid?: boolean; solarWaterHeater?: boolean;
|
|
122
|
+
electricityDuty?: string; otherCharges?: string; unpaidPrincipal?: string; lateMonths?: string;
|
|
123
|
+
}) => {
|
|
124
|
+
const runtime = await readyRuntime();
|
|
125
|
+
const overrides: Partial<DomesticTariffInput> = {};
|
|
126
|
+
if (o.units !== undefined) overrides.unitsKwh = cliNumber("units", o.units);
|
|
127
|
+
if (o.load !== undefined) overrides.sanctionedLoadKw = cliNumber("load", o.load);
|
|
128
|
+
if (o.prepaid) overrides.prepaid = true;
|
|
129
|
+
if (o.solarWaterHeater) overrides.solarWaterHeaterEligible = true;
|
|
130
|
+
if (o.electricityDuty !== undefined) overrides.electricityDutyAmount = cliNumber("electricity-duty", o.electricityDuty);
|
|
131
|
+
if (o.otherCharges !== undefined) overrides.otherChargesAmount = cliNumber("other-charges", o.otherCharges);
|
|
132
|
+
if (o.unpaidPrincipal !== undefined) overrides.unpaidPrincipalAmount = cliNumber("unpaid-principal", o.unpaidPrincipal);
|
|
133
|
+
if (o.lateMonths !== undefined) overrides.lateMonths = cliNumber("late-months", o.lateMonths);
|
|
134
|
+
print(await runtime.tariffEstimate(o.account, overrides));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
program.command("bills")
|
|
138
|
+
.option("--account <id>")
|
|
139
|
+
.option("--from <yyyy-mm-dd>")
|
|
140
|
+
.option("--to <yyyy-mm-dd>")
|
|
141
|
+
.action(async (o: DateRangeOptions) => {
|
|
142
|
+
assertDateRange(o.from, o.to);
|
|
143
|
+
const runtime = await readyRuntime();
|
|
144
|
+
print(await runtime.main.history("BILL", o.account, o.from, o.to));
|
|
145
|
+
});
|
|
146
|
+
program.command("payments")
|
|
147
|
+
.option("--account <id>")
|
|
148
|
+
.option("--from <yyyy-mm-dd>")
|
|
149
|
+
.option("--to <yyyy-mm-dd>")
|
|
150
|
+
.action(async (o: DateRangeOptions) => {
|
|
151
|
+
assertDateRange(o.from, o.to);
|
|
152
|
+
const runtime = await readyRuntime();
|
|
153
|
+
print(await runtime.main.history("PAYM", o.account, o.from, o.to));
|
|
154
|
+
});
|
|
155
|
+
program.command("consumption")
|
|
156
|
+
.option("--account <id>")
|
|
157
|
+
.option("--from <yyyy-mm-dd>")
|
|
158
|
+
.option("--to <yyyy-mm-dd>")
|
|
159
|
+
.action(async (o: DateRangeOptions) => {
|
|
160
|
+
assertDateRange(o.from, o.to);
|
|
161
|
+
const runtime = await readyRuntime();
|
|
162
|
+
print(await runtime.main.consumption(o.account, o.from, o.to));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const smart = program.command("smart").description("Genus smart-meter records, readings, reports, and status evidence");
|
|
166
|
+
smart.command("connect")
|
|
167
|
+
.option("--account <id>")
|
|
168
|
+
.option("--meter <number>")
|
|
169
|
+
.action(async (o: { account?: string; meter?: string }) => {
|
|
170
|
+
const runtime = await readyRuntime();
|
|
171
|
+
const client = await runtime.ensureSmart(o.account, o.meter);
|
|
172
|
+
print({ connected: Boolean(client.bearerToken), tokenSaved: true });
|
|
173
|
+
});
|
|
174
|
+
smart.command("session").description("Show smart-meter SSO account/session metadata").action(async () => {
|
|
175
|
+
const runtime = await readyRuntime();
|
|
176
|
+
print(await (await runtime.ensureSmart()).connections());
|
|
177
|
+
});
|
|
178
|
+
smart.command("dashboard").description("Measured/account smart-meter digest; predictions and tips are excluded by default")
|
|
179
|
+
.option("--account <id>")
|
|
180
|
+
.option("--include-derived", "include forecasts, estimates, insights, and smart tips")
|
|
181
|
+
.action(async (o: { account?: string; includeDerived?: boolean }) => {
|
|
182
|
+
const runtime = await readyRuntime();
|
|
183
|
+
print(await (await runtime.ensureSmart(o.account)).dashboard(o.account, { includeDerived: o.includeDerived }));
|
|
184
|
+
});
|
|
185
|
+
smart.command("meter").description("Meter technical profile and current reading")
|
|
186
|
+
.option("--account <id>").option("--meter <number>")
|
|
187
|
+
.action(async (o: { account?: string; meter?: string }) => {
|
|
188
|
+
const runtime = await readyRuntime();
|
|
189
|
+
const client = await runtime.ensureSmart(o.account, o.meter);
|
|
190
|
+
const accountId = o.account ?? client.accountId;
|
|
191
|
+
const meterNumber = o.meter ?? client.meterNumber;
|
|
192
|
+
if (!accountId || !meterNumber) throw new JpdclError("Account ID and meter number are required", 400);
|
|
193
|
+
const [details, reading] = await Promise.all([
|
|
194
|
+
client.request("smart_meter_details", { params: { meterNumber } }),
|
|
195
|
+
client.request("smart_current_meter_reading", { params: { accountId } }),
|
|
196
|
+
]);
|
|
197
|
+
print({ details: details.data, currentReading: reading.data });
|
|
198
|
+
});
|
|
199
|
+
smart.command("health").description("Unified supply, connectivity evidence, on-demand readings, voltage, outages, daily ledger, and alarms")
|
|
200
|
+
.option("--account <id>").action(async (o: { account?: string }) => {
|
|
201
|
+
const runtime = await readyRuntime();
|
|
202
|
+
print(await runtime.meterHealth(o.account));
|
|
203
|
+
});
|
|
204
|
+
smart.command("alerts").description("Live usage and daily/monthly alert thresholds")
|
|
205
|
+
.option("--account <id>").action(async (o: { account?: string }) => {
|
|
206
|
+
const runtime = await readyRuntime();
|
|
207
|
+
const client = await runtime.ensureSmart(o.account);
|
|
208
|
+
if (!client.accountId || !client.meterNumber) throw new JpdclError("Smart account is incomplete", 400);
|
|
209
|
+
print(await client.request("smart_my_alerts", { params: { accountId: client.accountId, meterNumber: client.meterNumber } }));
|
|
210
|
+
});
|
|
211
|
+
smart.command("billing").description("Postpaid bills/payments or prepaid balance/recharges, selected automatically")
|
|
212
|
+
.option("--account <id>").action(async (o: { account?: string }) => {
|
|
213
|
+
const runtime = await readyRuntime();
|
|
214
|
+
const client = await runtime.ensureSmart(o.account);
|
|
215
|
+
if (!client.accountId || !client.meterNumber) throw new JpdclError("Smart account is incomplete", 400);
|
|
216
|
+
const isPrepaid = String(client.claims.currentAccountIsMeterPrepaid).toLowerCase() === "true";
|
|
217
|
+
if (isPrepaid) {
|
|
218
|
+
const [balance, rechargeBalance, recharges, bills] = await Promise.all([
|
|
219
|
+
client.request("smart_prepaid_balance", { params: { meterNumber: client.meterNumber } }),
|
|
220
|
+
client.request("smart_prepaid_recharge_balance", { params: { accountId: client.accountId } }),
|
|
221
|
+
client.request("smart_prepaid_recharge_history", { params: { meterNumber: client.meterNumber } }),
|
|
222
|
+
client.request("smart_prepaid_bill_history", { params: { meterNumber: client.meterNumber } }),
|
|
223
|
+
]);
|
|
224
|
+
print({ plan: "prepaid", balance: balance.data, rechargeBalance: rechargeBalance.data, recharges: recharges.data, bills: bills.data });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const [lastBill, bills, payments] = await Promise.all([
|
|
228
|
+
client.request("smart_postpaid_last_bill", { params: { accountId: client.accountId } }),
|
|
229
|
+
client.request("smart_postpaid_bill_history", { params: { accountId: client.accountId } }),
|
|
230
|
+
client.request("smart_postpaid_payment_history", { params: { accountId: client.accountId } }),
|
|
231
|
+
]);
|
|
232
|
+
print({ plan: "postpaid", lastBill: lastBill.data, bills: bills.data, payments: payments.data });
|
|
233
|
+
});
|
|
234
|
+
smart.command("preferences").description("Every notification category and delivery-channel preference")
|
|
235
|
+
.option("--account <id>").action(async (o: { account?: string }) => {
|
|
236
|
+
const runtime = await readyRuntime();
|
|
237
|
+
const client = await runtime.ensureSmart(o.account);
|
|
238
|
+
const isPrepaid = String(client.claims.currentAccountIsMeterPrepaid).toLowerCase() === "true";
|
|
239
|
+
print(await client.request("smart_preferences", { params: { isPrepaid } }));
|
|
240
|
+
});
|
|
241
|
+
smart.command("notifications").description("Notifications and unread count")
|
|
242
|
+
.option("--account <id>").action(async (o: { account?: string }) => {
|
|
243
|
+
const runtime = await readyRuntime();
|
|
244
|
+
const client = await runtime.ensureSmart(o.account);
|
|
245
|
+
const userId = typeof client.claims.sub === "string" ? client.claims.sub : undefined;
|
|
246
|
+
if (!userId) throw new JpdclError("Smart user ID is unavailable", 400);
|
|
247
|
+
const [items, unread] = await Promise.all([
|
|
248
|
+
client.request("smart_notifications", { params: { userId } }).catch(() => ({ status: true, data: [] })),
|
|
249
|
+
client.request("smart_notification_unread_count", { params: { userId } }).catch(() => ({ status: true, data: { unreadCount: 0 } })),
|
|
250
|
+
]);
|
|
251
|
+
print({ items: items.data, unread: unread.data });
|
|
252
|
+
});
|
|
253
|
+
smart.command("support").description("FAQs, contact details, categories, and complaint history")
|
|
254
|
+
.option("--account <id>").option("--page <number>", "page number", "1").option("--page-size <number>", "records per page", "20")
|
|
255
|
+
.action(async (o: { account?: string; page: string; pageSize: string }) => {
|
|
256
|
+
const runtime = await readyRuntime();
|
|
257
|
+
const client = await runtime.ensureSmart(o.account);
|
|
258
|
+
const userId = typeof client.claims.sub === "string" ? client.claims.sub : undefined;
|
|
259
|
+
if (!userId) throw new JpdclError("Smart user ID is unavailable", 400);
|
|
260
|
+
const [faqs, contact, categories, complaints] = await Promise.all([
|
|
261
|
+
client.request("smart_faqs"), client.request("smart_contact_support").catch(() => ({ status: true, data: null })), client.request("smart_complaint_categories"),
|
|
262
|
+
client.request("smart_complaints", { params: { userId, pageNumber: Number(o.page), pageSize: Number(o.pageSize) } }),
|
|
263
|
+
]);
|
|
264
|
+
print({ faqs: faqs.data, contact: contact.data, categories: categories.data, complaints: complaints.data });
|
|
265
|
+
});
|
|
266
|
+
smart.command("consumption")
|
|
267
|
+
.option("--account <id>")
|
|
268
|
+
.option("--type <type>", "daily, weekly, or monthly", "monthly")
|
|
269
|
+
.option("--value <number>", "number of periods", "12")
|
|
270
|
+
.action(async (o: { account?: string; type: string; value: string }) => {
|
|
271
|
+
const runtime = await readyRuntime();
|
|
272
|
+
print(await (await runtime.ensureSmart(o.account)).consumption(o.account, o.type, Number(o.value)));
|
|
273
|
+
});
|
|
274
|
+
smart.command("intervals")
|
|
275
|
+
.option("--account <id>")
|
|
276
|
+
.requiredOption("--from <yyyy-mm-dd>")
|
|
277
|
+
.requiredOption("--to <yyyy-mm-dd>")
|
|
278
|
+
.option("--sort <order>", "date or another portal-supported order", "date")
|
|
279
|
+
.action(async (o: { account?: string; from: string; to: string; sort: string }) => {
|
|
280
|
+
assertDateRange(o.from, o.to, { requireBoth: true });
|
|
281
|
+
const runtime = await readyRuntime();
|
|
282
|
+
print(await (await runtime.ensureSmart(o.account)).intervalConsumption(o.account, o.from, o.to, o.sort));
|
|
283
|
+
});
|
|
284
|
+
smart.command("report")
|
|
285
|
+
.option("--account <id>")
|
|
286
|
+
.requiredOption("--type <type>", "monthly-tod, daily-tod, power-events, peak-slots, peak-monthly, voltage, or demand")
|
|
287
|
+
.option("--from <yyyy-mm-dd>")
|
|
288
|
+
.option("--to <yyyy-mm-dd>")
|
|
289
|
+
.option("--start <number>", "first record", "1")
|
|
290
|
+
.option("--end <number>", "last record")
|
|
291
|
+
.option("--filter <value>", "advanced: pre-encoded portal report filter")
|
|
292
|
+
.option("--format <format>", "xlsx or pdf")
|
|
293
|
+
.action(async (o: { account?: string; type: string; from?: string; to?: string; start: string; end?: string; filter?: string; format?: "xlsx" | "pdf" }) => {
|
|
294
|
+
assertDateRange(o.from, o.to, { pairedWhenPresent: true });
|
|
295
|
+
const names = {
|
|
296
|
+
"monthly-tod": "MonthlyTOD",
|
|
297
|
+
"daily-tod": "DayWiseTOD",
|
|
298
|
+
"power-events": "PowerOnOff",
|
|
299
|
+
"peak-slots": "PeakSlotConsumption",
|
|
300
|
+
"peak-monthly": "PeakSlotConsumptionMonthly",
|
|
301
|
+
voltage: "ConsumerVoltageDataProfile",
|
|
302
|
+
demand: "SanctionLoadVSMaxDemand",
|
|
303
|
+
} as const;
|
|
304
|
+
const name = names[o.type as keyof typeof names];
|
|
305
|
+
if (!name) throw new JpdclError("Unknown report type", 400);
|
|
306
|
+
const runtime = await readyRuntime();
|
|
307
|
+
print(await (await runtime.ensureSmart(o.account)).report(name, o.account, {
|
|
308
|
+
from: o.from,
|
|
309
|
+
to: o.to,
|
|
310
|
+
start: Number(o.start),
|
|
311
|
+
end: o.end ? Number(o.end) : undefined,
|
|
312
|
+
filter: o.filter,
|
|
313
|
+
format: o.format,
|
|
314
|
+
}));
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const ledger = program.command("ledger").description("Public JPDCL daily smart-meter import/export register ledger");
|
|
318
|
+
ledger.command("readings")
|
|
319
|
+
.option("--account <id>")
|
|
320
|
+
.option("--from <yyyy-mm-dd>")
|
|
321
|
+
.option("--to <yyyy-mm-dd>")
|
|
322
|
+
.option("--limit <count>", "maximum daily rows returned", "35")
|
|
323
|
+
.action(async (o: { account?: string; from?: string; to?: string; limit: string }) => {
|
|
324
|
+
assertDateRange(o.from, o.to);
|
|
325
|
+
const runtime = await readyRuntime();
|
|
326
|
+
print(await runtime.energyLedger(o.account, { from: o.from, to: o.to, limit: cliNumber("limit", o.limit) }));
|
|
327
|
+
});
|
|
328
|
+
ledger.command("summary")
|
|
329
|
+
.option("--account <id>")
|
|
330
|
+
.option("--month <yyyy-mm>", "defaults to the latest available ledger month")
|
|
331
|
+
.action(async (o: { account?: string; month?: string }) => {
|
|
332
|
+
if (o.month && !/^\d{4}-\d{2}$/.test(o.month)) throw new JpdclError("--month must be YYYY-MM", 400);
|
|
333
|
+
const runtime = await readyRuntime();
|
|
334
|
+
const from = o.month ? `${o.month}-01` : undefined;
|
|
335
|
+
const to = o.month ? `${o.month}-31` : undefined;
|
|
336
|
+
const result = await runtime.energyLedger(o.account, { from, to, limit: 0 });
|
|
337
|
+
print(result);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
program.command("request")
|
|
341
|
+
.description("Call any catalog endpoint directly")
|
|
342
|
+
.argument("<endpoint>")
|
|
343
|
+
.option("--params <json>", "URL parameter JSON", "{}")
|
|
344
|
+
.option("--body <json>", "request body/filter JSON", "{}")
|
|
345
|
+
.option("--output <file>", "write base64 binary data to a file")
|
|
346
|
+
.option("--confirm", "confirm this exact account-changing endpoint")
|
|
347
|
+
.action(async (name: string, options: { params: string; body: string; output?: string; confirm?: boolean }) => {
|
|
348
|
+
if (!isEndpointName(name)) throw new JpdclError(`Unknown endpoint: ${name}`, 400);
|
|
349
|
+
const runtime = await readyRuntime();
|
|
350
|
+
const request = { params: JSON.parse(options.params), body: JSON.parse(options.body) };
|
|
351
|
+
const definition = endpointCatalog[name];
|
|
352
|
+
if (definition.mutation && !options.confirm) {
|
|
353
|
+
throw new JpdclError("Mutating endpoints require --confirm and JPDCL_ENABLE_MUTATIONS=true", 400);
|
|
354
|
+
}
|
|
355
|
+
const result = definition.portal === "main"
|
|
356
|
+
? await runtime.main.request(name, request)
|
|
357
|
+
: definition.portal === "smart"
|
|
358
|
+
? await (await runtime.ensureSmart()).request(name, request)
|
|
359
|
+
: await runtime.ledger.request(name, request);
|
|
360
|
+
if (options.output) {
|
|
361
|
+
const data = (result as { data?: { base64?: string } | string }).data;
|
|
362
|
+
const base64 = typeof data === "string" ? data : data?.base64;
|
|
363
|
+
if (!base64) throw new JpdclError("The response did not contain base64 file data", 422);
|
|
364
|
+
await fs.writeFile(options.output, Buffer.from(base64, "base64"));
|
|
365
|
+
print({ saved: options.output });
|
|
366
|
+
} else print(result);
|
|
367
|
+
await runtime.persist();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
interface DateRangeOptions {
|
|
371
|
+
account?: string;
|
|
372
|
+
from?: string;
|
|
373
|
+
to?: string;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function cliNumber(name: string, value: string): number {
|
|
377
|
+
const parsed = Number(value);
|
|
378
|
+
if (!Number.isFinite(parsed) || parsed < 0) throw new JpdclError(`--${name} must be a non-negative number`, 400);
|
|
379
|
+
return parsed;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function readyRuntime(): Promise<JpdclRuntime> {
|
|
383
|
+
const runtime = await JpdclRuntime.create();
|
|
384
|
+
await runtime.ensureLogin();
|
|
385
|
+
return runtime;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
program.parseAsync().catch((error: unknown) => {
|
|
389
|
+
const known = error instanceof JpdclError;
|
|
390
|
+
process.stderr.write(`${JSON.stringify({
|
|
391
|
+
error: known ? error.name : "Error",
|
|
392
|
+
message: error instanceof Error ? error.message : String(error),
|
|
393
|
+
status: known ? error.status : 500,
|
|
394
|
+
details: known ? error.details : undefined,
|
|
395
|
+
}, null, 2)}\n`);
|
|
396
|
+
process.exitCode = 1;
|
|
397
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const MAIN_API_URL = "https://mob.jpdcl.co.in/WSSJPDCL/JWTAUTHENC/API";
|
|
5
|
+
export const SMART_API_URL = "https://cp.rdssjpdcl.com/api";
|
|
6
|
+
export const PORTAL_BASIC = "portal:portal123";
|
|
7
|
+
export const LOGIN_CREDENTIAL_HEADER = "mhgj70aizasybty01ob6mfvqoh0fj6rwvjluukcw8mjr04pkjh";
|
|
8
|
+
|
|
9
|
+
export const defaultSessionFile = process.env.JPDCL_SESSION_FILE
|
|
10
|
+
? path.resolve(process.env.JPDCL_SESSION_FILE)
|
|
11
|
+
: path.join(os.homedir(), ".jpdcl", "session.json");
|
|
12
|
+
|
|
13
|
+
export function mutationsEnabled(): boolean {
|
|
14
|
+
return process.env.JPDCL_ENABLE_MUTATIONS?.toLowerCase() === "true";
|
|
15
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { config as loadDotEnv } from "dotenv";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
let loadedEnv: Record<string, string> | undefined;
|
|
6
|
+
let credentialsExistedBeforeDotEnv = false;
|
|
7
|
+
let dotenvLoaded = false;
|
|
8
|
+
|
|
9
|
+
function configuredEnvFile(): string {
|
|
10
|
+
return process.env.JPDCL_ENV_FILE
|
|
11
|
+
? path.resolve(process.env.JPDCL_ENV_FILE)
|
|
12
|
+
: path.resolve(process.cwd(), ".env");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ensureDotEnvLoaded(): void {
|
|
16
|
+
if (dotenvLoaded) return;
|
|
17
|
+
credentialsExistedBeforeDotEnv = Boolean(process.env.JPDCL_LOGIN_ID && process.env.JPDCL_PASSWORD);
|
|
18
|
+
loadedEnv = loadDotEnv({ path: configuredEnvFile(), override: false, quiet: true }).parsed;
|
|
19
|
+
dotenvLoaded = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Credentials {
|
|
23
|
+
loginId: string;
|
|
24
|
+
password: string;
|
|
25
|
+
source: "environment" | "dotenv";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function resolveCredentials(): Promise<Credentials | undefined> {
|
|
29
|
+
ensureDotEnvLoaded();
|
|
30
|
+
const loginId = process.env.JPDCL_LOGIN_ID;
|
|
31
|
+
const password = process.env.JPDCL_PASSWORD;
|
|
32
|
+
if (!loginId || !password) return undefined;
|
|
33
|
+
const loadedFromDotEnv = !credentialsExistedBeforeDotEnv
|
|
34
|
+
&& loadedEnv?.JPDCL_LOGIN_ID === loginId
|
|
35
|
+
&& loadedEnv?.JPDCL_PASSWORD === password;
|
|
36
|
+
return {
|
|
37
|
+
loginId,
|
|
38
|
+
password,
|
|
39
|
+
source: loadedFromDotEnv ? "dotenv" : "environment",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function storeEnvCredentials(
|
|
44
|
+
loginId: string,
|
|
45
|
+
password: string,
|
|
46
|
+
targetFile = configuredEnvFile(),
|
|
47
|
+
): Promise<string> {
|
|
48
|
+
const absoluteTarget = path.resolve(targetFile);
|
|
49
|
+
await fs.mkdir(path.dirname(absoluteTarget), { recursive: true, mode: 0o700 });
|
|
50
|
+
|
|
51
|
+
let existing = "";
|
|
52
|
+
try {
|
|
53
|
+
existing = await fs.readFile(absoluteTarget, "utf8");
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const kept = existing
|
|
59
|
+
.split(/\r?\n/)
|
|
60
|
+
.filter((line) => !/^\s*(?:export\s+)?JPDCL_(?:LOGIN_ID|PASSWORD)\s*=/.test(line));
|
|
61
|
+
while (kept.length && !kept.at(-1)?.trim()) kept.pop();
|
|
62
|
+
const credentials = [
|
|
63
|
+
`JPDCL_LOGIN_ID=${JSON.stringify(loginId)}`,
|
|
64
|
+
`JPDCL_PASSWORD=${JSON.stringify(password)}`,
|
|
65
|
+
];
|
|
66
|
+
const content = [...kept, ...(kept.length ? [""] : []), ...credentials, ""].join("\n");
|
|
67
|
+
await fs.writeFile(absoluteTarget, content, { mode: 0o600 });
|
|
68
|
+
await fs.chmod(absoluteTarget, 0o600);
|
|
69
|
+
|
|
70
|
+
process.env.JPDCL_LOGIN_ID = loginId;
|
|
71
|
+
process.env.JPDCL_PASSWORD = password;
|
|
72
|
+
loadedEnv = { ...loadedEnv, JPDCL_LOGIN_ID: loginId, JPDCL_PASSWORD: password };
|
|
73
|
+
credentialsExistedBeforeDotEnv = false;
|
|
74
|
+
dotenvLoaded = true;
|
|
75
|
+
return absoluteTarget;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function deleteEnvCredentials(targetFile = configuredEnvFile()): Promise<boolean> {
|
|
79
|
+
const absoluteTarget = path.resolve(targetFile);
|
|
80
|
+
let existing: string;
|
|
81
|
+
try {
|
|
82
|
+
existing = await fs.readFile(absoluteTarget, "utf8");
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
const lines = existing.split(/\r?\n/);
|
|
88
|
+
const filtered = lines.filter((line) => !/^\s*(?:export\s+)?JPDCL_(?:LOGIN_ID|PASSWORD)\s*=/.test(line));
|
|
89
|
+
const removed = filtered.length !== lines.length;
|
|
90
|
+
if (!removed) return false;
|
|
91
|
+
const remaining = filtered.join("\n").replace(/^\s+|\s+$/g, "");
|
|
92
|
+
if (remaining) {
|
|
93
|
+
await fs.writeFile(absoluteTarget, `${remaining}\n`, { mode: 0o600 });
|
|
94
|
+
await fs.chmod(absoluteTarget, 0o600);
|
|
95
|
+
} else {
|
|
96
|
+
await fs.unlink(absoluteTarget);
|
|
97
|
+
}
|
|
98
|
+
delete process.env.JPDCL_LOGIN_ID;
|
|
99
|
+
delete process.env.JPDCL_PASSWORD;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function credentialStatus(): Promise<{
|
|
104
|
+
automaticRelogin: boolean;
|
|
105
|
+
source?: Credentials["source"];
|
|
106
|
+
envFile?: string;
|
|
107
|
+
}> {
|
|
108
|
+
const credentials = await resolveCredentials();
|
|
109
|
+
return {
|
|
110
|
+
automaticRelogin: Boolean(credentials),
|
|
111
|
+
source: credentials?.source,
|
|
112
|
+
envFile: credentials?.source === "dotenv" ? configuredEnvFile() : undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import CryptoJS from "crypto-js";
|
|
2
|
+
|
|
3
|
+
const PASSPHRASE = "wertyuioplkjhgfdsazxcvbnmkiujnhy";
|
|
4
|
+
export const ENCRYPTED_FIELD = "khasgafsgauysuaysyuasyahghagsggadkdcnviw";
|
|
5
|
+
|
|
6
|
+
export function encryptPayload(payload: unknown): Record<string, string> {
|
|
7
|
+
const iv = CryptoJS.lib.WordArray.random(16);
|
|
8
|
+
const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(payload), PASSPHRASE, { iv });
|
|
9
|
+
return { [ENCRYPTED_FIELD]: iv.toString(CryptoJS.enc.Base64) + ciphertext.toString() };
|
|
10
|
+
}
|
|
11
|
+
export function decryptPayload<T = unknown>(payload: string): T {
|
|
12
|
+
if (!payload || payload.length < 25) {
|
|
13
|
+
throw new Error("JPDCL returned an invalid encrypted response");
|
|
14
|
+
}
|
|
15
|
+
const iv = CryptoJS.enc.Base64.parse(payload.slice(0, 24));
|
|
16
|
+
const ciphertext = payload.slice(24);
|
|
17
|
+
const bytes = CryptoJS.AES.decrypt(ciphertext, PASSPHRASE, { iv });
|
|
18
|
+
const plaintext = bytes.toString(CryptoJS.enc.Utf8);
|
|
19
|
+
if (!plaintext) throw new Error("Unable to decrypt the JPDCL response");
|
|
20
|
+
return JSON.parse(plaintext) as T;
|
|
21
|
+
}
|
package/src/dates.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { JpdclError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export function isIsoDate(value: string): boolean {
|
|
4
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
5
|
+
const timestamp = Date.parse(`${value}T00:00:00Z`);
|
|
6
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString().slice(0, 10) === value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function assertDateRange(
|
|
10
|
+
from?: string,
|
|
11
|
+
to?: string,
|
|
12
|
+
options: { requireBoth?: boolean; pairedWhenPresent?: boolean } = {},
|
|
13
|
+
): void {
|
|
14
|
+
if (from && !isIsoDate(from)) throw new JpdclError("from must be a real date in YYYY-MM-DD format", 400);
|
|
15
|
+
if (to && !isIsoDate(to)) throw new JpdclError("to must be a real date in YYYY-MM-DD format", 400);
|
|
16
|
+
if (options.requireBoth && (!from || !to)) throw new JpdclError("Both from and to dates are required", 400);
|
|
17
|
+
if (options.pairedWhenPresent && Boolean(from) !== Boolean(to)) throw new JpdclError("Supply both from and to dates, or neither", 400);
|
|
18
|
+
if (from && to && from > to) throw new JpdclError("from must be on or before to", 400);
|
|
19
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class JpdclError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
message: string,
|
|
4
|
+
public readonly status?: number,
|
|
5
|
+
public readonly details?: unknown,
|
|
6
|
+
) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "JpdclError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class AuthenticationError extends JpdclError {
|
|
12
|
+
constructor(message = "JPDCL authentication is required", details?: unknown) {
|
|
13
|
+
super(message, 401, details);
|
|
14
|
+
this.name = "AuthenticationError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class MutationDisabledError extends JpdclError {
|
|
19
|
+
constructor() {
|
|
20
|
+
super(
|
|
21
|
+
"Mutating JPDCL operations are disabled. Set JPDCL_ENABLE_MUTATIONS=true to enable them.",
|
|
22
|
+
403,
|
|
23
|
+
);
|
|
24
|
+
this.name = "MutationDisabledError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
2
|
+
import { createJpdclMcpServer } from "./mcp.js";
|
|
3
|
+
import { JpdclRuntime } from "./runtime.js";
|
|
4
|
+
|
|
5
|
+
const corsHeaders = {
|
|
6
|
+
"Access-Control-Allow-Origin": "*",
|
|
7
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
8
|
+
"Access-Control-Allow-Headers": "Content-Type, Accept, mcp-protocol-version, mcp-session-id, last-event-id, x-jpdcl-login-id, x-jpdcl-password",
|
|
9
|
+
"Access-Control-Expose-Headers": "mcp-protocol-version, mcp-session-id",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function withCors(response: Response): Response {
|
|
13
|
+
const headers = new Headers(response.headers);
|
|
14
|
+
for (const [name, value] of Object.entries(corsHeaders)) headers.set(name, value);
|
|
15
|
+
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function handleMcpRequest(request: Request): Promise<Response> {
|
|
19
|
+
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: corsHeaders });
|
|
20
|
+
|
|
21
|
+
const loginId = request.headers.get("x-jpdcl-login-id")?.trim();
|
|
22
|
+
const password = request.headers.get("x-jpdcl-password")?.trim();
|
|
23
|
+
if (Boolean(loginId) !== Boolean(password)) {
|
|
24
|
+
return withCors(Response.json({
|
|
25
|
+
jsonrpc: "2.0",
|
|
26
|
+
error: { code: -32602, message: "Both x-jpdcl-login-id and x-jpdcl-password are required" },
|
|
27
|
+
id: null,
|
|
28
|
+
}, { status: 400 }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const url = new URL(request.url);
|
|
32
|
+
const runtime = await JpdclRuntime.create({
|
|
33
|
+
credentials: loginId && password ? { loginId, password, source: "environment" } : undefined,
|
|
34
|
+
persistent: false,
|
|
35
|
+
allowMutations: url.searchParams.get("allowMutations") === "true",
|
|
36
|
+
});
|
|
37
|
+
const server = await createJpdclMcpServer({
|
|
38
|
+
runtime,
|
|
39
|
+
allowCredentialStorage: false,
|
|
40
|
+
credentialSource: loginId && password ? "smithery-connection" : undefined,
|
|
41
|
+
});
|
|
42
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
43
|
+
sessionIdGenerator: undefined,
|
|
44
|
+
enableJsonResponse: true,
|
|
45
|
+
});
|
|
46
|
+
await server.connect(transport);
|
|
47
|
+
return withCors(await transport.handleRequest(request));
|
|
48
|
+
}
|