@teamnetwork-nz/feenix-mcp-server 0.1.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.
Files changed (78) hide show
  1. package/README.md +217 -0
  2. package/dist/client.d.ts +28 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +89 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/config.d.ts +8 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +54 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/datetime.d.ts +4 -0
  11. package/dist/datetime.d.ts.map +1 -0
  12. package/dist/datetime.js +58 -0
  13. package/dist/datetime.js.map +1 -0
  14. package/dist/format.d.ts +10 -0
  15. package/dist/format.d.ts.map +1 -0
  16. package/dist/format.js +126 -0
  17. package/dist/format.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +80 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/schemas/quickorder.d.ts +568 -0
  23. package/dist/schemas/quickorder.d.ts.map +1 -0
  24. package/dist/schemas/quickorder.js +100 -0
  25. package/dist/schemas/quickorder.js.map +1 -0
  26. package/dist/tools/addresses.d.ts +3 -0
  27. package/dist/tools/addresses.d.ts.map +1 -0
  28. package/dist/tools/addresses.js +38 -0
  29. package/dist/tools/addresses.js.map +1 -0
  30. package/dist/tools/billing.d.ts +3 -0
  31. package/dist/tools/billing.d.ts.map +1 -0
  32. package/dist/tools/billing.js +33 -0
  33. package/dist/tools/billing.js.map +1 -0
  34. package/dist/tools/connection.d.ts +3 -0
  35. package/dist/tools/connection.d.ts.map +1 -0
  36. package/dist/tools/connection.js +38 -0
  37. package/dist/tools/connection.js.map +1 -0
  38. package/dist/tools/customers.d.ts +3 -0
  39. package/dist/tools/customers.d.ts.map +1 -0
  40. package/dist/tools/customers.js +72 -0
  41. package/dist/tools/customers.js.map +1 -0
  42. package/dist/tools/index.d.ts +3 -0
  43. package/dist/tools/index.d.ts.map +1 -0
  44. package/dist/tools/index.js +25 -0
  45. package/dist/tools/index.js.map +1 -0
  46. package/dist/tools/inventory.d.ts +3 -0
  47. package/dist/tools/inventory.d.ts.map +1 -0
  48. package/dist/tools/inventory.js +80 -0
  49. package/dist/tools/inventory.js.map +1 -0
  50. package/dist/tools/misc.d.ts +3 -0
  51. package/dist/tools/misc.d.ts.map +1 -0
  52. package/dist/tools/misc.js +38 -0
  53. package/dist/tools/misc.js.map +1 -0
  54. package/dist/tools/orders.d.ts +3 -0
  55. package/dist/tools/orders.d.ts.map +1 -0
  56. package/dist/tools/orders.js +138 -0
  57. package/dist/tools/orders.js.map +1 -0
  58. package/dist/tools/products.d.ts +3 -0
  59. package/dist/tools/products.d.ts.map +1 -0
  60. package/dist/tools/products.js +63 -0
  61. package/dist/tools/products.js.map +1 -0
  62. package/dist/tools/services.d.ts +3 -0
  63. package/dist/tools/services.d.ts.map +1 -0
  64. package/dist/tools/services.js +120 -0
  65. package/dist/tools/services.js.map +1 -0
  66. package/dist/tools/subscribers.d.ts +3 -0
  67. package/dist/tools/subscribers.d.ts.map +1 -0
  68. package/dist/tools/subscribers.js +94 -0
  69. package/dist/tools/subscribers.js.map +1 -0
  70. package/dist/tools/vendors.d.ts +3 -0
  71. package/dist/tools/vendors.d.ts.map +1 -0
  72. package/dist/tools/vendors.js +68 -0
  73. package/dist/tools/vendors.js.map +1 -0
  74. package/dist/types.d.ts +14 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +5 -0
  77. package/dist/types.js.map +1 -0
  78. package/package.json +41 -0
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # @teamnetwork-nz/feenix-mcp-server
2
+
3
+ MCP server for the **[FeeniX Public API](https://www.postman.com/feenix/feenix-s-public-workspace/collection/ldx652e/feenix-public-api-documentation)** — New Zealand fibre/broadband wholesale **ordering, provisioning, subscriber management and billing**.
4
+
5
+ Compatible with Claude Desktop, Claude Code, Cursor, Cline, and any MCP-capable client.
6
+
7
+ **Read-only by default.** Write operations (create/update/delete, quick orders, amendments, callbacks) are hidden until you set `FEENIX_READWRITE=true`.
8
+
9
+ ## Requirements
10
+
11
+ - Node.js 20+
12
+ - A FeeniX **API key** generated in the FeeniX web portal.
13
+
14
+ ## Configuration
15
+
16
+ Set these environment variables before starting the server. Authentication is a single
17
+ `api-key` header carried on every request — there is no token exchange.
18
+
19
+ ### Required
20
+
21
+ | Variable | Description |
22
+ |---|---|
23
+ | `FEENIX_API_KEY` | API key generated in the FeeniX portal (sent as the `api-key` header) |
24
+ | `FEENIX_HOST` | API host URL. Protocol optional (`https://` assumed). May be replaced by `FEENIX_BASE_URL`. |
25
+
26
+ ### Optional
27
+
28
+ | Variable | Default | Description |
29
+ |---|---|---|
30
+ | `FEENIX_PORT` | *(none)* | Port for the API host (usually `443`). Appended to `FEENIX_HOST` unless it already has a port. |
31
+ | `FEENIX_BASE_URL` | *(derived from host/port)* | Explicit base URL override, e.g. `https://api.feenix.co.nz:443` |
32
+ | `FEENIX_READWRITE` | `false` | `true` exposes and enables write tools |
33
+ | `FEENIX_DATETIMEFORMAT` | `local` | Timezone for date display — `local`, an IANA name (e.g. `Pacific/Auckland`), or `UTC` |
34
+ | `FEENIX_PAGESIZE` | `20` | Default page size for list tools that paginate server-side (inventory) |
35
+
36
+ ## Claude Desktop configuration
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "feenix": {
42
+ "command": "npx",
43
+ "args": ["-y", "@teamnetwork-nz/feenix-mcp-server"],
44
+ "env": {
45
+ "FEENIX_API_KEY": "your-api-key",
46
+ "FEENIX_HOST": "https://api.feenix.co.nz",
47
+ "FEENIX_PORT": "443"
48
+ }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Claude Code configuration
55
+
56
+ ```bash
57
+ claude mcp add feenix \
58
+ -e FEENIX_API_KEY=your-api-key \
59
+ -e FEENIX_HOST=https://api.feenix.co.nz \
60
+ -e FEENIX_PORT=443 \
61
+ -- npx -y @teamnetwork-nz/feenix-mcp-server
62
+ ```
63
+
64
+ ## Read-only mode (default)
65
+
66
+ The gate is enforced in two layers:
67
+
68
+ 1. **Tool visibility** — in read-only mode the write tools are not listed at all, so an
69
+ agent never sees create/update/delete or ordering operations.
70
+ 2. **Client guard** — even if invoked directly, the HTTP client's write helpers
71
+ (`post`/`put`/`patch`/`delete`) throw before any request is sent.
72
+
73
+ Read-only queries that happen to use `POST` (e.g. `feenix_bulk_data_usage`) are treated
74
+ as reads and remain available.
75
+
76
+ ## Available tools
77
+
78
+ Read tools are always available (28). Write tools (marked **[write]**, 34 more) require
79
+ `FEENIX_READWRITE=true`.
80
+
81
+ ### Connection
82
+ | Tool | Description |
83
+ |---|---|
84
+ | `feenix_test_connection` | Verify credentials; report base URL, mode, and API-key owner |
85
+ | `feenix_get_customer_from_apikey` | Full customer record the API key belongs to |
86
+
87
+ ### Addresses & prequalification
88
+ | Tool | Description |
89
+ |---|---|
90
+ | `feenix_search_addresses` | Free-text address search → TUIs |
91
+ | `feenix_address_prequal` | Available services/technologies for an address TUI |
92
+
93
+ ### Products
94
+ | Tool | Description |
95
+ |---|---|
96
+ | `feenix_list_products` | List the product catalogue |
97
+ | `feenix_get_product` | One product by `_id` |
98
+ | `feenix_get_product_variants_csv` | Product variants list (CSV) |
99
+ | `feenix_create_product` · `feenix_update_product` · `feenix_delete_product` | **[write]** catalogue CRUD |
100
+
101
+ ### Services (sold connections)
102
+ | Tool | Description |
103
+ |---|---|
104
+ | `feenix_list_services` · `feenix_get_service` | List / fetch sold services |
105
+ | `feenix_service_linetest` | Line test an active fibre connection (`soldServiceId`) |
106
+ | `feenix_create_service` · `feenix_update_service` · `feenix_delete_service` | **[write]** service CRUD |
107
+ | `feenix_change_product` · `feenix_change_attributes` | **[write]** modify a completed service |
108
+ | `feenix_suspend_service` · `feenix_decommission_service` | **[write]** suspend / decommission |
109
+
110
+ ### Orders (Quick Order, Tail Order, Modem Order)
111
+ | Tool | Description |
112
+ |---|---|
113
+ | `feenix_quickorder_search` | Search orders (case-sensitive criteria) |
114
+ | `feenix_quick_order` · `feenix_quick_order_subscriber` | **[write]** place an order |
115
+ | `feenix_amend_order` · `feenix_cancel_order` | **[write]** amend / cancel in-flight orders |
116
+ | `feenix_tailorder_run_feasibility` · `feenix_tailorder_get_available_appointments` · `feenix_tailorder_reserve_appointment` · `feenix_tailorder_service_given` · `feenix_tailorder_order` | **[write]** staged tail-order flow |
117
+ | `feenix_modemorder_complete` | **[write]** complete a modem order |
118
+
119
+ ### Subscribers (access identities)
120
+ | Tool | Description |
121
+ |---|---|
122
+ | `feenix_list_subscribers` · `feenix_get_subscriber` | List / fetch subscribers |
123
+ | `feenix_get_subscriber_status` · `feenix_get_subscriber_data_usage` | Status & data usage |
124
+ | `feenix_get_subscriber_logs` | RADIUS auth logs (by query) |
125
+ | `feenix_bulk_data_usage` | Bulk data-usage lookup (read via POST) |
126
+ | `feenix_create_subscriber` · `feenix_update_subscriber` · `feenix_delete_subscriber` | **[write]** subscriber CRUD |
127
+
128
+ ### Inventory
129
+ | Tool | Description |
130
+ |---|---|
131
+ | `feenix_list_inventory` | List inventory (filter by serial number, paginate) |
132
+ | `feenix_get_inventory` | One inventory record by `_id` |
133
+ | `feenix_create_inventory` · `feenix_update_inventory` · `feenix_delete_inventory` | **[write]** inventory CRUD |
134
+
135
+ ### Vendors
136
+ | Tool | Description |
137
+ |---|---|
138
+ | `feenix_list_vendors` · `feenix_get_vendor` | List / fetch vendors |
139
+ | `feenix_get_losing_service_provider` | LSP for a vendor + technology (churn) |
140
+ | `feenix_create_vendor` · `feenix_update_vendor` · `feenix_delete_vendor` | **[write]** vendor CRUD |
141
+
142
+ ### Customers & callbacks
143
+ | Tool | Description |
144
+ |---|---|
145
+ | `feenix_list_customers` · `feenix_get_customer` | List / fetch customers |
146
+ | `feenix_create_customer` · `feenix_update_customer` · `feenix_delete_customer` | **[write]** customer CRUD |
147
+ | `feenix_set_callback_url` · `feenix_test_callback` | **[write]** configure / test webhook callbacks |
148
+
149
+ ### Billing
150
+ | Tool | Description |
151
+ |---|---|
152
+ | `feenix_query_invoices` | Invoices between two dates |
153
+ | `feenix_get_invoice` | One invoice by number (`INV-0001`) or Xero UUID |
154
+
155
+ ### Voice & tasks
156
+ | Tool | Description |
157
+ |---|---|
158
+ | `feenix_get_call_details` | Call Detail Records (CDRs) for a number over a date range |
159
+ | `feenix_get_task_notifications` | Notifications raised for a request/task during ordering |
160
+
161
+ ## Agent guidance
162
+
163
+ ### Ordering flow
164
+ 1. `feenix_search_addresses` → find the address + TUI.
165
+ 2. `feenix_address_prequal` (by TUI) → available technologies/products.
166
+ 3. `feenix_list_products` → pick a product.
167
+ 4. `feenix_quick_order` **(write)** → place the order; the response returns tasks with ids.
168
+ 5. Tail/modem orders are multi-step: feasibility → available appointments → reserve → service given → order/complete.
169
+ 6. `feenix_quickorder_search` and `feenix_get_task_notifications` track progress.
170
+
171
+ ### Services vs subscribers
172
+ - A **sold service** (`soldServiceId`) is the provisioned connection — inspect, line-test, change, suspend or decommission it.
173
+ - A **subscriber** is the RADIUS/access identity — status, data usage and auth logs.
174
+
175
+ ### IDs
176
+ Most resources use Mongo-style `_id` hex strings. Invoices use invoice numbers or Xero
177
+ UUIDs. Addresses use TUIs.
178
+
179
+ ### List output
180
+ List tools (`feenix_list_*`) unwrap the FeeniX `{ serverSidePaginateInfo, models: [...] }`
181
+ envelope into a compact table of identity columns plus a total/pages footer. Use the
182
+ matching `feenix_get_*` tool for the full record.
183
+
184
+ ### Write bodies
185
+ - `feenix_quick_order` / `feenix_quick_order_subscriber` expose the **typed QuickOrder
186
+ fields** (address, soldProduct, tailProduct, subscriberName, username, contact details,
187
+ `siteInfo`, `tailOrder`, plus optional voice/modem/mesh objects). Any field not modelled
188
+ can still be supplied verbatim — the schema is permissive (passthrough).
189
+ - `feenix_change_product` and `feenix_change_attributes` are likewise typed.
190
+ - The remaining order tools (amend, cancel, tail-order steps, modem complete) accept a raw
191
+ JSON `body` object, because those documents are sparsely specified.
192
+
193
+ Consult the [FeeniX Postman documentation](https://www.postman.com/feenix/feenix-s-public-workspace/collection/ldx652e/feenix-public-api-documentation)
194
+ for exact field requirements per order type. Note: QuickOrder `address` is the address
195
+ **`_id`** from prequal (`feenix_address_prequal`), not the TUI.
196
+
197
+ ## Development
198
+
199
+ ```bash
200
+ npm install
201
+ npm run build
202
+
203
+ FEENIX_API_KEY=... \
204
+ FEENIX_HOST=https://api.feenix.co.nz \
205
+ FEENIX_PORT=443 \
206
+ node dist/index.js
207
+ ```
208
+
209
+ Test with the MCP inspector:
210
+
211
+ ```bash
212
+ npx @modelcontextprotocol/inspector node dist/index.js
213
+ ```
214
+
215
+ ## License
216
+
217
+ MIT
@@ -0,0 +1,28 @@
1
+ import { FeenixConfig } from './config.js';
2
+ /**
3
+ * Thin HTTP client for the FeeniX Public API.
4
+ *
5
+ * Authentication is a single `api-key` header carried on every request (no OAuth /
6
+ * token exchange). Writes are gated behind FEENIX_READWRITE=true.
7
+ */
8
+ export declare class FeenixClient {
9
+ private http;
10
+ private config;
11
+ constructor(config: FeenixConfig);
12
+ getBaseUrl(): string;
13
+ getDefaultPageSize(): number;
14
+ isReadWrite(): boolean;
15
+ private request;
16
+ private assertWritePermitted;
17
+ get<T>(path: string, params?: Record<string, unknown>): Promise<T>;
18
+ /**
19
+ * A POST used purely to *query* data (e.g. bulk data-usage lookups). These read
20
+ * endpoints happen to use POST for their request body, so they are NOT gated.
21
+ */
22
+ postQuery<T>(path: string, body: unknown): Promise<T>;
23
+ post<T>(path: string, body?: unknown): Promise<T>;
24
+ put<T>(path: string, body?: unknown): Promise<T>;
25
+ patch<T>(path: string, body: unknown): Promise<T>;
26
+ delete<T>(path: string): Promise<T>;
27
+ }
28
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;GAKG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAgB;IAC5B,OAAO,CAAC,MAAM,CAAe;gBAEjB,MAAM,EAAE,YAAY;IAYhC,UAAU,IAAI,MAAM;IAIpB,kBAAkB,IAAI,MAAM;IAI5B,WAAW,IAAI,OAAO;YAMR,OAAO;IAgCrB,OAAO,CAAC,oBAAoB;IAStB,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIxE;;;OAGG;IACG,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIrD,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAKjD,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAKhD,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAKjD,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;CAI1C"}
package/dist/client.js ADDED
@@ -0,0 +1,89 @@
1
+ import axios from 'axios';
2
+ /**
3
+ * Thin HTTP client for the FeeniX Public API.
4
+ *
5
+ * Authentication is a single `api-key` header carried on every request (no OAuth /
6
+ * token exchange). Writes are gated behind FEENIX_READWRITE=true.
7
+ */
8
+ export class FeenixClient {
9
+ http;
10
+ config;
11
+ constructor(config) {
12
+ this.config = config;
13
+ this.http = axios.create({
14
+ baseURL: config.baseUrl,
15
+ headers: {
16
+ 'api-key': config.apiKey,
17
+ 'Content-Type': 'application/json',
18
+ Accept: 'application/json',
19
+ },
20
+ });
21
+ }
22
+ getBaseUrl() {
23
+ return this.config.baseUrl;
24
+ }
25
+ getDefaultPageSize() {
26
+ return this.config.pageSize;
27
+ }
28
+ isReadWrite() {
29
+ return this.config.readWrite;
30
+ }
31
+ // ---- Core request ---------------------------------------------------------
32
+ async request(method, path, data, params) {
33
+ const cfg = { method, url: path, params, data };
34
+ try {
35
+ const resp = await this.http.request(cfg);
36
+ return resp.data;
37
+ }
38
+ catch (err) {
39
+ if (axios.isAxiosError(err)) {
40
+ const respData = err.response?.data;
41
+ const msg = respData?.['message'] ??
42
+ respData?.['error'] ??
43
+ respData?.['errorMessage'] ??
44
+ (typeof respData === 'string' ? respData : undefined) ??
45
+ err.message;
46
+ const status = err.response?.status;
47
+ const hint = status === 401 || status === 403
48
+ ? ' (check FEENIX_API_KEY and that the key has access to this resource)'
49
+ : '';
50
+ throw new Error(`FeeniX API error [${status ?? '?'}]: ${msg}${hint}`);
51
+ }
52
+ throw err;
53
+ }
54
+ }
55
+ // ---- Read-only gate -------------------------------------------------------
56
+ assertWritePermitted() {
57
+ if (!this.config.readWrite) {
58
+ throw new Error('This operation is a write and the server is running read-only. ' +
59
+ 'Set FEENIX_READWRITE=true to enable create/update/delete and ordering operations.');
60
+ }
61
+ }
62
+ async get(path, params) {
63
+ return this.request('GET', path, undefined, params);
64
+ }
65
+ /**
66
+ * A POST used purely to *query* data (e.g. bulk data-usage lookups). These read
67
+ * endpoints happen to use POST for their request body, so they are NOT gated.
68
+ */
69
+ async postQuery(path, body) {
70
+ return this.request('POST', path, body);
71
+ }
72
+ async post(path, body) {
73
+ this.assertWritePermitted();
74
+ return this.request('POST', path, body ?? {});
75
+ }
76
+ async put(path, body) {
77
+ this.assertWritePermitted();
78
+ return this.request('PUT', path, body ?? {});
79
+ }
80
+ async patch(path, body) {
81
+ this.assertWritePermitted();
82
+ return this.request('PATCH', path, body);
83
+ }
84
+ async delete(path) {
85
+ this.assertWritePermitted();
86
+ return this.request('DELETE', path);
87
+ }
88
+ }
89
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export interface FeenixConfig {
2
+ baseUrl: string;
3
+ apiKey: string;
4
+ readWrite: boolean;
5
+ pageSize: number;
6
+ }
7
+ export declare function loadConfig(): FeenixConfig;
8
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAeD,wBAAgB,UAAU,IAAI,YAAY,CAoCzC"}
package/dist/config.js ADDED
@@ -0,0 +1,54 @@
1
+ // Prepend https:// when the host is given without a protocol, and strip any trailing slash.
2
+ function normalizeHost(host) {
3
+ const withProto = /^https?:\/\//i.test(host) ? host : `https://${host}`;
4
+ return withProto.replace(/\/+$/, '');
5
+ }
6
+ // Does the host portion already carry an explicit :port ? (ignore the protocol's own colon)
7
+ function hasExplicitPort(host) {
8
+ const afterProto = host.replace(/^https?:\/\//i, '');
9
+ const hostOnly = afterProto.split('/')[0];
10
+ return /:\d+$/.test(hostOnly);
11
+ }
12
+ export function loadConfig() {
13
+ const apiKey = process.env.FEENIX_API_KEY;
14
+ const host = process.env.FEENIX_HOST?.trim();
15
+ const port = process.env.FEENIX_PORT?.trim();
16
+ const explicitBaseUrl = process.env.FEENIX_BASE_URL?.trim();
17
+ const missing = [];
18
+ if (!apiKey)
19
+ missing.push('FEENIX_API_KEY');
20
+ if (!host && !explicitBaseUrl)
21
+ missing.push('FEENIX_HOST (or FEENIX_BASE_URL)');
22
+ if (missing.length > 0) {
23
+ throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
24
+ }
25
+ // Resolve base URL: explicit override wins, otherwise compose host[:port].
26
+ let baseUrl;
27
+ if (explicitBaseUrl) {
28
+ baseUrl = explicitBaseUrl.replace(/\/+$/, '');
29
+ }
30
+ else {
31
+ const normalized = normalizeHost(host);
32
+ if (port && !hasExplicitPort(normalized)) {
33
+ baseUrl = `${normalized}:${port}`;
34
+ }
35
+ else {
36
+ baseUrl = normalized;
37
+ }
38
+ }
39
+ const readWrite = process.env.FEENIX_READWRITE?.toLowerCase() === 'true';
40
+ const pageSize = clampPageSize(process.env.FEENIX_PAGESIZE);
41
+ return {
42
+ baseUrl,
43
+ apiKey: apiKey,
44
+ readWrite,
45
+ pageSize,
46
+ };
47
+ }
48
+ function clampPageSize(raw) {
49
+ const n = raw ? Number(raw) : 20;
50
+ if (!Number.isFinite(n) || n <= 0)
51
+ return 20;
52
+ return Math.min(Math.floor(n), 200);
53
+ }
54
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAOA,4FAA4F;AAC5F,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,SAAS,GAAG,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,IAAI,EAAE,CAAC;IACxE,OAAO,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACvC,CAAC;AAED,4FAA4F;AAC5F,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1C,OAAO,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC;IAC7C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC;IAC7C,MAAM,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,EAAE,CAAC;IAE5D,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,CAAC,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC5C,IAAI,CAAC,IAAI,IAAI,CAAC,eAAe;QAAE,OAAO,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;IAEhF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,2CAA2C,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnF,CAAC;IAED,2EAA2E;IAC3E,IAAI,OAAe,CAAC;IACpB,IAAI,eAAe,EAAE,CAAC;QACpB,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAChD,CAAC;SAAM,CAAC;QACN,MAAM,UAAU,GAAG,aAAa,CAAC,IAAK,CAAC,CAAC;QACxC,IAAI,IAAI,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,EAAE,CAAC;YACzC,OAAO,GAAG,GAAG,UAAU,IAAI,IAAI,EAAE,CAAC;QACpC,CAAC;aAAM,CAAC;YACN,OAAO,GAAG,UAAU,CAAC;QACvB,CAAC;IACH,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,WAAW,EAAE,KAAK,MAAM,CAAC;IACzE,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAE5D,OAAO;QACL,OAAO;QACP,MAAM,EAAE,MAAO;QACf,SAAS;QACT,QAAQ;KACT,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,GAAuB;IAC5C,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACjC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAC7C,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACtC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function getConfiguredTz(): string;
2
+ export declare function fromUtc(value: string | number | null | undefined): string;
3
+ export declare function maybeFromUtc(val: unknown): unknown;
4
+ //# sourceMappingURL=datetime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datetime.d.ts","sourceRoot":"","sources":["../src/datetime.ts"],"names":[],"mappings":"AASA,wBAAgB,eAAe,IAAI,MAAM,CAGxC;AAID,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAuBzE;AAcD,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGlD"}
@@ -0,0 +1,58 @@
1
+ // Reads FEENIX_DATETIMEFORMAT (default: "local").
2
+ // "local" → system local timezone (Intl.DateTimeFormat default)
3
+ // <tz> → any IANA timezone name, e.g. "Pacific/Auckland", "America/New_York", "UTC"
4
+ //
5
+ // FeeniX timestamps are ISO-8601 strings (often UTC). This module renders them in the
6
+ // configured display timezone. Values that don't look like datetimes pass through.
7
+ const ISO_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/;
8
+ export function getConfiguredTz() {
9
+ const fmt = process.env.FEENIX_DATETIMEFORMAT ?? 'local';
10
+ return fmt === 'local' ? Intl.DateTimeFormat().resolvedOptions().timeZone : fmt;
11
+ }
12
+ // Convert a UTC/ISO datetime (or epoch seconds/millis number) into a display string
13
+ // in the configured timezone. Returns '' for null/undefined.
14
+ export function fromUtc(value) {
15
+ if (value === null || value === undefined || value === '')
16
+ return '';
17
+ try {
18
+ const d = toDate(value);
19
+ if (!d || isNaN(d.getTime()))
20
+ return String(value);
21
+ const tz = getConfiguredTz();
22
+ const parts = new Intl.DateTimeFormat('en-CA', {
23
+ timeZone: tz,
24
+ year: 'numeric',
25
+ month: '2-digit',
26
+ day: '2-digit',
27
+ hour: '2-digit',
28
+ minute: '2-digit',
29
+ second: '2-digit',
30
+ hour12: false,
31
+ timeZoneName: 'short',
32
+ }).formatToParts(d);
33
+ const get = (type) => parts.find((p) => p.type === type)?.value ?? '';
34
+ const h = get('hour') === '24' ? '00' : get('hour');
35
+ return `${get('year')}-${get('month')}-${get('day')} ${h}:${get('minute')}:${get('second')} ${get('timeZoneName')}`;
36
+ }
37
+ catch {
38
+ return String(value);
39
+ }
40
+ }
41
+ function toDate(value) {
42
+ if (typeof value === 'number') {
43
+ const ms = value < 1e12 ? value * 1000 : value;
44
+ return new Date(ms);
45
+ }
46
+ const s = value.trim();
47
+ if (/^\d+(\.\d+)?$/.test(s))
48
+ return toDate(Number(s));
49
+ return new Date(s);
50
+ }
51
+ // Apply fromUtc to any value that looks like an ISO datetime; pass everything else
52
+ // through unchanged.
53
+ export function maybeFromUtc(val) {
54
+ if (typeof val !== 'string')
55
+ return val;
56
+ return ISO_RE.test(val.trim()) ? fromUtc(val) : val;
57
+ }
58
+ //# sourceMappingURL=datetime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"datetime.js","sourceRoot":"","sources":["../src/datetime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export declare function jsonBlock(title: string, data: unknown): string;
2
+ export declare function table(title: string, headers: string[], rows: string[][]): string;
3
+ export declare function cell(v: unknown): string;
4
+ /**
5
+ * Render a FeeniX list response as a compact table. Handles both the paginated
6
+ * envelope `{ serverSidePaginateInfo, models: [...] }` and a bare array. Anything
7
+ * that isn't list-shaped falls back to jsonBlock.
8
+ */
9
+ export declare function modelsList(title: string, data: unknown, columns?: string[]): string;
10
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAmBA,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAgB9D;AAGD,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,CAUhF;AAGD,wBAAgB,IAAI,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAIvC;AAoCD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CA6BnF"}
package/dist/format.js ADDED
@@ -0,0 +1,126 @@
1
+ import { maybeFromUtc } from './datetime.js';
2
+ const MAX_OUTPUT_CHARS = 40_000;
3
+ // Recursively convert ISO datetime strings within any value to the configured timezone.
4
+ function convertDates(v) {
5
+ if (Array.isArray(v))
6
+ return v.map(convertDates);
7
+ if (v && typeof v === 'object') {
8
+ const out = {};
9
+ for (const [k, val] of Object.entries(v)) {
10
+ out[k] = convertDates(val);
11
+ }
12
+ return out;
13
+ }
14
+ return maybeFromUtc(v);
15
+ }
16
+ // Render an arbitrary API result as a titled, pretty-printed JSON block. Arrays get a
17
+ // count in the heading. Oversized payloads are truncated with a note.
18
+ export function jsonBlock(title, data) {
19
+ const converted = convertDates(data);
20
+ const count = Array.isArray(converted) ? ` (${converted.length})` : '';
21
+ let body;
22
+ try {
23
+ body = JSON.stringify(converted, null, 2);
24
+ }
25
+ catch {
26
+ body = String(converted);
27
+ }
28
+ if (body === undefined || body === 'undefined')
29
+ body = '(no content)';
30
+ let note = '';
31
+ if (body.length > MAX_OUTPUT_CHARS) {
32
+ body = body.slice(0, MAX_OUTPUT_CHARS);
33
+ note = '\n\n_Output truncated — narrow the query (filter, date range, or pagination) for the full record._';
34
+ }
35
+ return `**${title}${count}**\n\n\`\`\`json\n${body}\n\`\`\`${note}`;
36
+ }
37
+ // Render headers + rows as a Markdown table. Empty rows → "No records" line.
38
+ export function table(title, headers, rows) {
39
+ if (!rows.length)
40
+ return `**${title}**\n\nNo records found.`;
41
+ const sep = headers.map(() => '---');
42
+ const body = rows.map((r) => `| ${r.map((c) => c ?? '').join(' | ')} |`);
43
+ return [
44
+ `**${title} (${rows.length})**\n`,
45
+ `| ${headers.join(' | ')} |`,
46
+ `| ${sep.join(' | ')} |`,
47
+ ...body,
48
+ ].join('\n');
49
+ }
50
+ // Coerce anything to a short display string for table cells.
51
+ export function cell(v) {
52
+ if (v === null || v === undefined)
53
+ return '';
54
+ if (typeof v === 'object')
55
+ return JSON.stringify(v);
56
+ return String(maybeFromUtc(v));
57
+ }
58
+ // Resolve a dotted path (e.g. "asset.serialNumber") against an object.
59
+ function getPath(obj, path) {
60
+ return path.split('.').reduce((acc, key) => {
61
+ if (acc && typeof acc === 'object')
62
+ return acc[key];
63
+ return undefined;
64
+ }, obj);
65
+ }
66
+ // Preferred identity columns, in priority order, when a caller doesn't specify any.
67
+ const PRIORITY_COLS = [
68
+ '_id',
69
+ 'name',
70
+ 'shortName',
71
+ 'subscriberName',
72
+ 'username',
73
+ 'number',
74
+ 'productClass',
75
+ 'category',
76
+ 'status',
77
+ 'country',
78
+ ];
79
+ function autoColumns(sample) {
80
+ const scalars = Object.entries(sample)
81
+ .filter(([, v]) => v === null || ['string', 'number', 'boolean'].includes(typeof v))
82
+ .map(([k]) => k);
83
+ const chosen = PRIORITY_COLS.filter((c) => c in sample);
84
+ for (const k of scalars) {
85
+ if (chosen.length >= 6)
86
+ break;
87
+ if (!chosen.includes(k))
88
+ chosen.push(k);
89
+ }
90
+ return chosen.length ? chosen.slice(0, 6) : ['_id'];
91
+ }
92
+ /**
93
+ * Render a FeeniX list response as a compact table. Handles both the paginated
94
+ * envelope `{ serverSidePaginateInfo, models: [...] }` and a bare array. Anything
95
+ * that isn't list-shaped falls back to jsonBlock.
96
+ */
97
+ export function modelsList(title, data, columns) {
98
+ let items;
99
+ let meta;
100
+ if (data && typeof data === 'object' && Array.isArray(data.models)) {
101
+ items = data.models;
102
+ meta = data.serverSidePaginateInfo;
103
+ }
104
+ else if (Array.isArray(data)) {
105
+ items = data;
106
+ }
107
+ else {
108
+ return jsonBlock(title, data);
109
+ }
110
+ if (!items.length)
111
+ return `**${title}**\n\nNo records found.`;
112
+ const first = items[0];
113
+ if (!first || typeof first !== 'object')
114
+ return jsonBlock(title, data);
115
+ const cols = columns ?? autoColumns(first);
116
+ const rows = items.map((m) => cols.map((c) => cell(getPath(m, c))));
117
+ const out = table(title, cols, rows);
118
+ if (meta && (meta['total'] !== undefined || meta['pages'] !== undefined)) {
119
+ const total = meta['total'] ?? items.length;
120
+ const page = meta['page'] ?? 1;
121
+ const pages = meta['pages'] ?? 1;
122
+ return `${out}\n\n_${total} total record(s), page ${page} of ${pages}. Use get-by-id for full detail._`;
123
+ }
124
+ return out;
125
+ }
126
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC,wFAAwF;AACxF,SAAS,YAAY,CAAC,CAAU;IAC9B,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACjD,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,GAAG,GAA4B,EAAE,CAAC;QACxC,KAAK,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAA4B,CAAC,EAAE,CAAC;YACpE,GAAG,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IACD,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC;AACzB,CAAC;AAED,sFAAsF;AACtF,sEAAsE;AACtE,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,IAAa;IACpD,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;IACD,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,WAAW;QAAE,IAAI,GAAG,cAAc,CAAC;IACtE,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,IAAI,CAAC,MAAM,GAAG,gBAAgB,EAAE,CAAC;QACnC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC;QACvC,IAAI,GAAG,oGAAoG,CAAC;IAC9G,CAAC;IACD,OAAO,KAAK,KAAK,GAAG,KAAK,qBAAqB,IAAI,WAAW,IAAI,EAAE,CAAC;AACtE,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,KAAK,CAAC,KAAa,EAAE,OAAiB,EAAE,IAAgB;IACtE,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,KAAK,yBAAyB,CAAC;IAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACzE,OAAO;QACL,KAAK,KAAK,KAAK,IAAI,CAAC,MAAM,OAAO;QACjC,KAAK,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI;QAC5B,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI;QACxB,GAAG,IAAI;KACR,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,IAAI,CAAC,CAAU;IAC7B,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAC7C,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,uEAAuE;AACvE,SAAS,OAAO,CAAC,GAAY,EAAE,IAAY;IACzC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAU,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAClD,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAQ,GAA+B,CAAC,GAAG,CAAC,CAAC;QACjF,OAAO,SAAS,CAAC;IACnB,CAAC,EAAE,GAAG,CAAC,CAAC;AACV,CAAC;AAED,oFAAoF;AACpF,MAAM,aAAa,GAAG;IACpB,KAAK;IACL,MAAM;IACN,WAAW;IACX,gBAAgB;IAChB,UAAU;IACV,QAAQ;IACR,cAAc;IACd,UAAU;IACV,QAAQ;IACR,SAAS;CACV,CAAC;AAEF,SAAS,WAAW,CAAC,MAA+B;IAClD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;SACnC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;SACnF,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IACnB,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC;IACxD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC;YAAE,MAAM;QAC9B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,IAAa,EAAE,OAAkB;IACzE,IAAI,KAAgB,CAAC;IACrB,IAAI,IAAyC,CAAC;IAE9C,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAE,IAAgC,CAAC,MAAM,CAAC,EAAE,CAAC;QAChG,KAAK,GAAI,IAAgC,CAAC,MAAmB,CAAC;QAC9D,IAAI,GAAI,IAAgC,CAAC,sBAA6D,CAAC;IACzG,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,KAAK,GAAG,IAAI,CAAC;IACf,CAAC;SAAM,CAAC;QACN,OAAO,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAChC,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,KAAK,KAAK,yBAAyB,CAAC;IAE9D,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACvB,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAEvE,MAAM,IAAI,GAAG,OAAO,IAAI,WAAW,CAAC,KAAgC,CAAC,CAAC;IACtE,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEpE,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACrC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,SAAS,CAAC,EAAE,CAAC;QACzE,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC;QAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACjC,OAAO,GAAG,GAAG,QAAQ,KAAK,0BAA0B,IAAI,OAAO,KAAK,mCAAmC,CAAC;IAC1G,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}