@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.
- package/README.md +217 -0
- package/dist/client.d.ts +28 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +89 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +54 -0
- package/dist/config.js.map +1 -0
- package/dist/datetime.d.ts +4 -0
- package/dist/datetime.d.ts.map +1 -0
- package/dist/datetime.js +58 -0
- package/dist/datetime.js.map +1 -0
- package/dist/format.d.ts +10 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +126 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/quickorder.d.ts +568 -0
- package/dist/schemas/quickorder.d.ts.map +1 -0
- package/dist/schemas/quickorder.js +100 -0
- package/dist/schemas/quickorder.js.map +1 -0
- package/dist/tools/addresses.d.ts +3 -0
- package/dist/tools/addresses.d.ts.map +1 -0
- package/dist/tools/addresses.js +38 -0
- package/dist/tools/addresses.js.map +1 -0
- package/dist/tools/billing.d.ts +3 -0
- package/dist/tools/billing.d.ts.map +1 -0
- package/dist/tools/billing.js +33 -0
- package/dist/tools/billing.js.map +1 -0
- package/dist/tools/connection.d.ts +3 -0
- package/dist/tools/connection.d.ts.map +1 -0
- package/dist/tools/connection.js +38 -0
- package/dist/tools/connection.js.map +1 -0
- package/dist/tools/customers.d.ts +3 -0
- package/dist/tools/customers.d.ts.map +1 -0
- package/dist/tools/customers.js +72 -0
- package/dist/tools/customers.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +25 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/inventory.d.ts +3 -0
- package/dist/tools/inventory.d.ts.map +1 -0
- package/dist/tools/inventory.js +80 -0
- package/dist/tools/inventory.js.map +1 -0
- package/dist/tools/misc.d.ts +3 -0
- package/dist/tools/misc.d.ts.map +1 -0
- package/dist/tools/misc.js +38 -0
- package/dist/tools/misc.js.map +1 -0
- package/dist/tools/orders.d.ts +3 -0
- package/dist/tools/orders.d.ts.map +1 -0
- package/dist/tools/orders.js +138 -0
- package/dist/tools/orders.js.map +1 -0
- package/dist/tools/products.d.ts +3 -0
- package/dist/tools/products.d.ts.map +1 -0
- package/dist/tools/products.js +63 -0
- package/dist/tools/products.js.map +1 -0
- package/dist/tools/services.d.ts +3 -0
- package/dist/tools/services.d.ts.map +1 -0
- package/dist/tools/services.js +120 -0
- package/dist/tools/services.js.map +1 -0
- package/dist/tools/subscribers.d.ts +3 -0
- package/dist/tools/subscribers.d.ts.map +1 -0
- package/dist/tools/subscribers.js +94 -0
- package/dist/tools/subscribers.js.map +1 -0
- package/dist/tools/vendors.d.ts +3 -0
- package/dist/tools/vendors.d.ts.map +1 -0
- package/dist/tools/vendors.js +68 -0
- package/dist/tools/vendors.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- 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
|
package/dist/client.d.ts
ADDED
|
@@ -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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"}
|
package/dist/config.d.ts
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/dist/datetime.js
ADDED
|
@@ -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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"}
|
package/dist/format.d.ts
ADDED
|
@@ -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":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|