@nocville/node 0.0.0-stage → 0.2.1
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 +67 -2
- package/dist/cjs/index.js +373 -0
- package/dist/cjs/package.json +1 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.js +369 -0
- package/package.json +48 -3
package/README.md
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nocville/node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Node.js SDK for pushing status updates to Nocville.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @nocville/node
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { NocvilleClient } from '@nocville/node';
|
|
15
|
+
|
|
16
|
+
const client = new NocvilleClient({
|
|
17
|
+
apiKey: 'your-api-key',
|
|
18
|
+
serviceId: 'my-service',
|
|
19
|
+
// apiUrl defaults to https://nocville.com
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Push a status update
|
|
23
|
+
const response = await client.pushStatus({
|
|
24
|
+
status: 'healthy',
|
|
25
|
+
mood: 'happy',
|
|
26
|
+
message: 'All systems operational',
|
|
27
|
+
metrics: {
|
|
28
|
+
cpu: 45,
|
|
29
|
+
memory: 60,
|
|
30
|
+
requests: 1250,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Quick helpers
|
|
35
|
+
await client.healthy('Everything is fine');
|
|
36
|
+
await client.warning('High memory usage');
|
|
37
|
+
await client.critical('Service down!');
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
| Option | Type | Default | Description |
|
|
43
|
+
| ------------ | ------- | ---------------------- | ------------------------------------------- |
|
|
44
|
+
| `apiKey` | string | required | Team API key for authentication |
|
|
45
|
+
| `apiUrl` | string | `https://nocville.com` | Nocville API URL (override for development) |
|
|
46
|
+
| `serviceId` | string | required | The service ID this client represents |
|
|
47
|
+
| `timeout` | number | 5000 | Request timeout in milliseconds |
|
|
48
|
+
| `retries` | number | 3 | Number of retry attempts on failure |
|
|
49
|
+
| `retryDelay` | number | 1000 | Base delay between retries in milliseconds |
|
|
50
|
+
| `debug` | boolean | false | Enable debug logging |
|
|
51
|
+
|
|
52
|
+
## Status Values
|
|
53
|
+
|
|
54
|
+
- `healthy` - Service is operating normally
|
|
55
|
+
- `warning` - Service has non-critical issues
|
|
56
|
+
- `critical` - Service has critical issues
|
|
57
|
+
- `unknown` - Service status cannot be determined
|
|
58
|
+
|
|
59
|
+
## Mood Values
|
|
60
|
+
|
|
61
|
+
- `happy` - NPC appears happy (typically for healthy status)
|
|
62
|
+
- `concerned` - NPC appears concerned (typically for warning status)
|
|
63
|
+
- `stressed` - NPC appears stressed (typically for critical status)
|
|
64
|
+
- `idle` - NPC appears neutral (typically for unknown status)
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Nocville Status Pusher SDK for Node.js
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import { NocvilleClient } from '@nocville/node';
|
|
8
|
+
*
|
|
9
|
+
* const client = new NocvilleClient({
|
|
10
|
+
* apiKey: 'your-api-key',
|
|
11
|
+
* serviceId: 'my-service',
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* // Push a status update
|
|
15
|
+
* await client.pushStatus({
|
|
16
|
+
* status: 'healthy',
|
|
17
|
+
* mood: 'happy',
|
|
18
|
+
* message: 'All systems operational',
|
|
19
|
+
* metrics: {
|
|
20
|
+
* cpu: 45,
|
|
21
|
+
* memory: 60,
|
|
22
|
+
* requests: 1250,
|
|
23
|
+
* },
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.NocvilleClient = void 0;
|
|
29
|
+
class StatusPushError extends Error {
|
|
30
|
+
constructor(statusCode, body) {
|
|
31
|
+
super(`HTTP ${statusCode}: ${body}`);
|
|
32
|
+
this.statusCode = statusCode;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
class NocvilleClient {
|
|
36
|
+
constructor(config) {
|
|
37
|
+
this.lastStatus = 'unknown';
|
|
38
|
+
this.lastMood = 'idle';
|
|
39
|
+
this.lastUpdate = 0;
|
|
40
|
+
this._links = [];
|
|
41
|
+
this._actions = [];
|
|
42
|
+
this._logBuffer = [];
|
|
43
|
+
this._heartbeatInterval = null;
|
|
44
|
+
this.config = {
|
|
45
|
+
apiUrl: 'https://nocville.com',
|
|
46
|
+
timeout: 5000,
|
|
47
|
+
retries: 3,
|
|
48
|
+
retryDelay: 1000,
|
|
49
|
+
debug: false,
|
|
50
|
+
...config,
|
|
51
|
+
};
|
|
52
|
+
this.config.apiUrl = this.config.apiUrl.replace(/\/+$/, '');
|
|
53
|
+
if (!this.config.apiKey) {
|
|
54
|
+
throw new Error('apiKey is required');
|
|
55
|
+
}
|
|
56
|
+
if (!this.config.serviceId) {
|
|
57
|
+
throw new Error('serviceId is required');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Push a status update to the Nocville server
|
|
62
|
+
*/
|
|
63
|
+
async pushStatus(update) {
|
|
64
|
+
const payload = {
|
|
65
|
+
serviceId: this.config.serviceId,
|
|
66
|
+
status: update.status,
|
|
67
|
+
mood: update.mood || this.moodFromStatus(update.status),
|
|
68
|
+
message: update.message,
|
|
69
|
+
metrics: update.metrics,
|
|
70
|
+
};
|
|
71
|
+
if (update.agentStats !== undefined)
|
|
72
|
+
payload.agentStats = update.agentStats;
|
|
73
|
+
if (update.appearance !== undefined)
|
|
74
|
+
payload.appearance = update.appearance;
|
|
75
|
+
if (update.name !== undefined)
|
|
76
|
+
payload.name = update.name;
|
|
77
|
+
if (update.archetype !== undefined)
|
|
78
|
+
payload.archetype = update.archetype;
|
|
79
|
+
if (update.subStatus !== undefined)
|
|
80
|
+
payload.subStatus = update.subStatus;
|
|
81
|
+
if (update.screenData !== undefined)
|
|
82
|
+
payload.screenData = update.screenData;
|
|
83
|
+
if (update.alertConfig !== undefined)
|
|
84
|
+
payload.alertConfig = update.alertConfig;
|
|
85
|
+
if (update.behavior !== undefined)
|
|
86
|
+
payload.behavior = update.behavior;
|
|
87
|
+
if (update.companion !== undefined)
|
|
88
|
+
payload.companion = update.companion;
|
|
89
|
+
if (update.offline !== undefined)
|
|
90
|
+
payload.offline = update.offline;
|
|
91
|
+
if (update.map !== undefined)
|
|
92
|
+
payload.map = update.map;
|
|
93
|
+
// Merge stored links/actions with per-call overrides
|
|
94
|
+
const links = update.links || (this._links.length > 0 ? this._links : undefined);
|
|
95
|
+
const actions = update.actions || (this._actions.length > 0 ? this._actions : undefined);
|
|
96
|
+
if (links)
|
|
97
|
+
payload.links = links;
|
|
98
|
+
if (actions)
|
|
99
|
+
payload.actions = actions;
|
|
100
|
+
let lastError = null;
|
|
101
|
+
for (let attempt = 0; attempt <= this.config.retries; attempt++) {
|
|
102
|
+
try {
|
|
103
|
+
if (attempt > 0) {
|
|
104
|
+
const delay = this.config.retryDelay * Math.pow(2, attempt - 1);
|
|
105
|
+
this.log(`Retry attempt ${attempt} after ${delay}ms`);
|
|
106
|
+
await this.sleep(delay);
|
|
107
|
+
}
|
|
108
|
+
const response = await this.makeRequest(payload);
|
|
109
|
+
this.lastStatus = update.status;
|
|
110
|
+
this.lastMood = update.mood || this.moodFromStatus(update.status);
|
|
111
|
+
this.lastUpdate = Date.now();
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
timestamp: this.lastUpdate,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
lastError = error;
|
|
119
|
+
this.log(`Request failed: ${lastError.message}`);
|
|
120
|
+
if (lastError instanceof StatusPushError &&
|
|
121
|
+
[400, 401, 403, 404].includes(lastError.statusCode))
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
error: lastError?.message || 'Unknown error',
|
|
129
|
+
statusCode: lastError instanceof StatusPushError ? lastError.statusCode : undefined,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Quick status helpers
|
|
134
|
+
*/
|
|
135
|
+
async healthy(message, metrics) {
|
|
136
|
+
return this.pushStatus({ status: 'healthy', mood: 'happy', message, metrics });
|
|
137
|
+
}
|
|
138
|
+
async warning(message, metrics) {
|
|
139
|
+
return this.pushStatus({ status: 'warning', mood: 'concerned', message, metrics });
|
|
140
|
+
}
|
|
141
|
+
async critical(message, metrics) {
|
|
142
|
+
return this.pushStatus({ status: 'critical', mood: 'stressed', message, metrics });
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get the last known status
|
|
146
|
+
*/
|
|
147
|
+
getLastStatus() {
|
|
148
|
+
return {
|
|
149
|
+
status: this.lastStatus,
|
|
150
|
+
timestamp: this.lastUpdate,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Push screen data for NPC display
|
|
155
|
+
*/
|
|
156
|
+
async pushScreen(data) {
|
|
157
|
+
return this.pushStatus({
|
|
158
|
+
status: this.lastStatus,
|
|
159
|
+
mood: this.lastMood,
|
|
160
|
+
screenData: data,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Push metrics as a multi-metric gauge layout
|
|
165
|
+
*/
|
|
166
|
+
async pushGauges(metrics) {
|
|
167
|
+
const screenData = {
|
|
168
|
+
layout: 'multi-metric',
|
|
169
|
+
values: Object.entries(metrics).map(([label, value]) => ({
|
|
170
|
+
label,
|
|
171
|
+
value: String(value),
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
174
|
+
return this.pushScreen(screenData);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Push a ticker-style screen display
|
|
178
|
+
*/
|
|
179
|
+
async pushTicker(values) {
|
|
180
|
+
const screenData = {
|
|
181
|
+
layout: 'ticker',
|
|
182
|
+
values: values.map((v) => ({
|
|
183
|
+
label: v.label,
|
|
184
|
+
value: v.value,
|
|
185
|
+
trend: v.trend,
|
|
186
|
+
})),
|
|
187
|
+
};
|
|
188
|
+
return this.pushScreen(screenData);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Push a log line to a rolling terminal display (keeps last 20 lines)
|
|
192
|
+
*/
|
|
193
|
+
async pushLogLine(line) {
|
|
194
|
+
this._logBuffer.push(line);
|
|
195
|
+
if (this._logBuffer.length > 20) {
|
|
196
|
+
this._logBuffer = this._logBuffer.slice(-20);
|
|
197
|
+
}
|
|
198
|
+
const screenData = {
|
|
199
|
+
layout: 'log-terminal',
|
|
200
|
+
values: [{ label: 'log', value: this._logBuffer.join('\n') }],
|
|
201
|
+
};
|
|
202
|
+
return this.pushScreen(screenData);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Start a heartbeat that periodically pushes the current status
|
|
206
|
+
*/
|
|
207
|
+
startHeartbeat(intervalMs = 30000) {
|
|
208
|
+
this.stopHeartbeat();
|
|
209
|
+
this._heartbeatInterval = setInterval(() => {
|
|
210
|
+
this.pushStatus({
|
|
211
|
+
status: this.lastStatus,
|
|
212
|
+
mood: this.lastMood,
|
|
213
|
+
}).catch((err) => this.log(`Heartbeat push failed: ${err}`));
|
|
214
|
+
}, intervalMs);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Stop the heartbeat interval
|
|
218
|
+
*/
|
|
219
|
+
stopHeartbeat() {
|
|
220
|
+
if (this._heartbeatInterval !== null) {
|
|
221
|
+
clearInterval(this._heartbeatInterval);
|
|
222
|
+
this._heartbeatInterval = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Set links to be included in every subsequent push
|
|
227
|
+
*/
|
|
228
|
+
setLinks(links) {
|
|
229
|
+
this._links = links;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Set actions to be included in every subsequent push
|
|
233
|
+
*/
|
|
234
|
+
setActions(actions) {
|
|
235
|
+
this._actions = actions;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Open an incident for this service
|
|
239
|
+
*/
|
|
240
|
+
async openIncident(title, severity = 'critical') {
|
|
241
|
+
const url = `${this.config.apiUrl}/api/v1/teams/_/incidents/sdk`;
|
|
242
|
+
const controller = new AbortController();
|
|
243
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
244
|
+
try {
|
|
245
|
+
const response = await fetch(url, {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: {
|
|
248
|
+
'Content-Type': 'application/json',
|
|
249
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
250
|
+
},
|
|
251
|
+
body: JSON.stringify({
|
|
252
|
+
serviceId: this.config.serviceId,
|
|
253
|
+
title,
|
|
254
|
+
severity,
|
|
255
|
+
}),
|
|
256
|
+
signal: controller.signal,
|
|
257
|
+
});
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
const errorText = await response.text();
|
|
260
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
261
|
+
}
|
|
262
|
+
const result = (await response.json());
|
|
263
|
+
this.log(`Incident opened: ${result.data.id}`);
|
|
264
|
+
return { id: result.data.id };
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
clearTimeout(timeoutId);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Resolve an incident by ID
|
|
272
|
+
*/
|
|
273
|
+
async resolveIncident(incidentId) {
|
|
274
|
+
const url = `${this.config.apiUrl}/api/v1/teams/_/incidents/${incidentId}/sdk`;
|
|
275
|
+
const controller = new AbortController();
|
|
276
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
277
|
+
try {
|
|
278
|
+
const response = await fetch(url, {
|
|
279
|
+
method: 'PATCH',
|
|
280
|
+
headers: {
|
|
281
|
+
'Content-Type': 'application/json',
|
|
282
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
283
|
+
},
|
|
284
|
+
body: JSON.stringify({ status: 'resolved' }),
|
|
285
|
+
signal: controller.signal,
|
|
286
|
+
});
|
|
287
|
+
if (!response.ok) {
|
|
288
|
+
const errorText = await response.text();
|
|
289
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
290
|
+
}
|
|
291
|
+
this.log(`Incident resolved: ${incidentId}`);
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
clearTimeout(timeoutId);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Add a note to an incident timeline
|
|
299
|
+
*/
|
|
300
|
+
async addIncidentNote(incidentId, note) {
|
|
301
|
+
const url = `${this.config.apiUrl}/api/v1/teams/_/incidents/${incidentId}/events/sdk`;
|
|
302
|
+
const controller = new AbortController();
|
|
303
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
304
|
+
try {
|
|
305
|
+
const response = await fetch(url, {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
headers: {
|
|
308
|
+
'Content-Type': 'application/json',
|
|
309
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
310
|
+
},
|
|
311
|
+
body: JSON.stringify({
|
|
312
|
+
type: 'note',
|
|
313
|
+
data: { note },
|
|
314
|
+
}),
|
|
315
|
+
signal: controller.signal,
|
|
316
|
+
});
|
|
317
|
+
if (!response.ok) {
|
|
318
|
+
const errorText = await response.text();
|
|
319
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
320
|
+
}
|
|
321
|
+
this.log(`Note added to incident: ${incidentId}`);
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
clearTimeout(timeoutId);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
moodFromStatus(status) {
|
|
328
|
+
switch (status) {
|
|
329
|
+
case 'healthy':
|
|
330
|
+
return 'happy';
|
|
331
|
+
case 'warning':
|
|
332
|
+
return 'concerned';
|
|
333
|
+
case 'critical':
|
|
334
|
+
return 'stressed';
|
|
335
|
+
default:
|
|
336
|
+
return 'idle';
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async makeRequest(payload) {
|
|
340
|
+
const url = `${this.config.apiUrl}/api/v1/status`;
|
|
341
|
+
const controller = new AbortController();
|
|
342
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
343
|
+
try {
|
|
344
|
+
const response = await fetch(url, {
|
|
345
|
+
method: 'POST',
|
|
346
|
+
headers: {
|
|
347
|
+
'Content-Type': 'application/json',
|
|
348
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
349
|
+
},
|
|
350
|
+
body: JSON.stringify(payload),
|
|
351
|
+
signal: controller.signal,
|
|
352
|
+
});
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
const errorText = await response.text();
|
|
355
|
+
throw new StatusPushError(response.status, errorText);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
clearTimeout(timeoutId);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
sleep(ms) {
|
|
363
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
364
|
+
}
|
|
365
|
+
log(message) {
|
|
366
|
+
if (this.config.debug) {
|
|
367
|
+
console.log(`[nocville] ${message}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
exports.NocvilleClient = NocvilleClient;
|
|
372
|
+
// Default export for convenience
|
|
373
|
+
exports.default = NocvilleClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nocville Status Pusher SDK for Node.js
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { NocvilleClient } from '@nocville/node';
|
|
7
|
+
*
|
|
8
|
+
* const client = new NocvilleClient({
|
|
9
|
+
* apiKey: 'your-api-key',
|
|
10
|
+
* serviceId: 'my-service',
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* // Push a status update
|
|
14
|
+
* await client.pushStatus({
|
|
15
|
+
* status: 'healthy',
|
|
16
|
+
* mood: 'happy',
|
|
17
|
+
* message: 'All systems operational',
|
|
18
|
+
* metrics: {
|
|
19
|
+
* cpu: 45,
|
|
20
|
+
* memory: 60,
|
|
21
|
+
* requests: 1250,
|
|
22
|
+
* },
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export type Status = 'healthy' | 'warning' | 'critical' | 'unknown';
|
|
27
|
+
export type Mood = 'happy' | 'concerned' | 'stressed' | 'idle';
|
|
28
|
+
export interface ScreenValue {
|
|
29
|
+
label: string;
|
|
30
|
+
value: string;
|
|
31
|
+
color?: string;
|
|
32
|
+
trend?: 'up' | 'down' | 'flat';
|
|
33
|
+
sparkline?: number[];
|
|
34
|
+
}
|
|
35
|
+
export interface ScreenData {
|
|
36
|
+
layout: string;
|
|
37
|
+
values: ScreenValue[];
|
|
38
|
+
}
|
|
39
|
+
export interface NPCLink {
|
|
40
|
+
label: string;
|
|
41
|
+
url: string;
|
|
42
|
+
icon?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface NPCAction {
|
|
45
|
+
id: string;
|
|
46
|
+
label: string;
|
|
47
|
+
type: 'confirm' | 'webhook' | 'link';
|
|
48
|
+
url?: string;
|
|
49
|
+
requiresAdmin?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface AlertConfig {
|
|
52
|
+
notifyOnCritical?: boolean;
|
|
53
|
+
soundEnabled?: boolean;
|
|
54
|
+
autoAcknowledgeAfter?: number;
|
|
55
|
+
heartbeatInterval?: number;
|
|
56
|
+
}
|
|
57
|
+
/** Companion pixel-screen types the server knows how to render. */
|
|
58
|
+
export type CompanionScreenType = 'log-terminal' | 'ticker-tape' | 'crt-monitor' | 'dashboard-wall' | 'status-light' | 'gauge-panel' | 'web-view' | 'desk-sign' | 'commit-board' | 'welcome-sign';
|
|
59
|
+
export type BehaviorActivity = 'busy' | 'idle' | 'waiting' | 'critical';
|
|
60
|
+
export type BehaviorIdle = 'seek-lounge' | 'seek-desk' | 'wander' | 'stay';
|
|
61
|
+
export type BehaviorPose = 'sit' | 'sleep' | 'stand';
|
|
62
|
+
export type CompanionPlacement = 'adjacent-wall' | 'on-desk' | 'freestanding';
|
|
63
|
+
export interface BehaviorPatrol {
|
|
64
|
+
/** [minMinutes, maxMinutes] between patrols */
|
|
65
|
+
everyMinutes: [number, number];
|
|
66
|
+
/** Number of stops per patrol */
|
|
67
|
+
stops?: number;
|
|
68
|
+
}
|
|
69
|
+
export type BehaviorHome = {
|
|
70
|
+
near: 'entrance' | 'center';
|
|
71
|
+
} | {
|
|
72
|
+
x: number;
|
|
73
|
+
y: number;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Behavior hints that steer how the NPC moves and idles in the world.
|
|
77
|
+
* All fields optional; the server merges hints → archetype defaults → global defaults.
|
|
78
|
+
*/
|
|
79
|
+
export interface BehaviorHints {
|
|
80
|
+
/** Schema version */
|
|
81
|
+
v?: number;
|
|
82
|
+
/** Explicit activity override; else derived server-side */
|
|
83
|
+
activity?: BehaviorActivity;
|
|
84
|
+
/** Where to go when idle */
|
|
85
|
+
idle?: BehaviorIdle;
|
|
86
|
+
/** Where to go when busy: 'seek-desk' | 'wander' | 'stay' | 'seek-object:<category|catalogId>' */
|
|
87
|
+
busy?: string;
|
|
88
|
+
/** Pose to strike when idle */
|
|
89
|
+
idlePose?: BehaviorPose;
|
|
90
|
+
/** Emote to show while waiting (e.g. 'raised-hand') */
|
|
91
|
+
waitingEmote?: string;
|
|
92
|
+
/** Greet players on first appearance */
|
|
93
|
+
greetPlayers?: boolean;
|
|
94
|
+
/** Patrol schedule */
|
|
95
|
+
patrol?: BehaviorPatrol;
|
|
96
|
+
/** Home base to return to */
|
|
97
|
+
home?: BehaviorHome;
|
|
98
|
+
}
|
|
99
|
+
export interface CompanionSize {
|
|
100
|
+
/** Width in tiles (1-6) */
|
|
101
|
+
w: number;
|
|
102
|
+
/** Height in tiles (1-6) */
|
|
103
|
+
h: number;
|
|
104
|
+
}
|
|
105
|
+
/** A companion pixel-art object that spawns alongside the NPC. */
|
|
106
|
+
export interface CompanionSpec {
|
|
107
|
+
screenType: CompanionScreenType;
|
|
108
|
+
size?: CompanionSize;
|
|
109
|
+
placement: CompanionPlacement;
|
|
110
|
+
label?: string;
|
|
111
|
+
}
|
|
112
|
+
export interface NocvilleConfig {
|
|
113
|
+
/** Nocville API URL (default: https://nocville.com) */
|
|
114
|
+
apiUrl?: string;
|
|
115
|
+
/** Team API key for authentication */
|
|
116
|
+
apiKey: string;
|
|
117
|
+
/** The service ID this client represents */
|
|
118
|
+
serviceId: string;
|
|
119
|
+
/** Request timeout in milliseconds (default: 5000) */
|
|
120
|
+
timeout?: number;
|
|
121
|
+
/** Number of retry attempts on failure (default: 3) */
|
|
122
|
+
retries?: number;
|
|
123
|
+
/** Base delay between retries in milliseconds (default: 1000) */
|
|
124
|
+
retryDelay?: number;
|
|
125
|
+
/** Enable debug logging (default: false) */
|
|
126
|
+
debug?: boolean;
|
|
127
|
+
}
|
|
128
|
+
export interface AgentStats {
|
|
129
|
+
filesChanged: number;
|
|
130
|
+
insertions: number;
|
|
131
|
+
deletions: number;
|
|
132
|
+
lastFiles: string[];
|
|
133
|
+
/** Reserved for future usage reporting; the hook collector does not compute these. */
|
|
134
|
+
tokensIn?: number;
|
|
135
|
+
tokensOut?: number;
|
|
136
|
+
costUsd?: number;
|
|
137
|
+
}
|
|
138
|
+
export interface StatusUpdate {
|
|
139
|
+
agentStats?: AgentStats;
|
|
140
|
+
/** The current status of the service */
|
|
141
|
+
status: Status;
|
|
142
|
+
/** The mood/emotional state for NPC visualization */
|
|
143
|
+
mood?: Mood;
|
|
144
|
+
/** Optional message to display */
|
|
145
|
+
message?: string;
|
|
146
|
+
/** Friendly display name shown on the NPC banner (falls back to serviceId). */
|
|
147
|
+
name?: string;
|
|
148
|
+
/** Optional metrics to track */
|
|
149
|
+
metrics?: Record<string, number | string>;
|
|
150
|
+
/** NPC archetype identifier */
|
|
151
|
+
archetype?: string;
|
|
152
|
+
/** Sub-status for more granular state */
|
|
153
|
+
subStatus?: string;
|
|
154
|
+
/** Screen data for NPC display */
|
|
155
|
+
screenData?: ScreenData;
|
|
156
|
+
/** Links associated with this NPC */
|
|
157
|
+
links?: NPCLink[];
|
|
158
|
+
/** Actions available on this NPC */
|
|
159
|
+
actions?: NPCAction[];
|
|
160
|
+
/** Alert configuration */
|
|
161
|
+
alertConfig?: AlertConfig;
|
|
162
|
+
/** Behavior hints steering NPC movement/idle/busy */
|
|
163
|
+
behavior?: BehaviorHints;
|
|
164
|
+
/** Companion pixel-art object spec */
|
|
165
|
+
companion?: CompanionSpec;
|
|
166
|
+
/**
|
|
167
|
+
* Appearance hint. `family` dresses an ai-agent NPC in its branded look
|
|
168
|
+
* ('claude' = terracotta Claude tee, 'codex' = black OpenAI tee).
|
|
169
|
+
*/
|
|
170
|
+
appearance?: {
|
|
171
|
+
family?: 'claude' | 'codex' | 'gemini' | 'cursor';
|
|
172
|
+
};
|
|
173
|
+
/** Clean-shutdown sentinel — prompts the server to despawn the NPC promptly */
|
|
174
|
+
offline?: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Target map for this push — a map id (e.g. `map_abc123`) or a map name
|
|
177
|
+
* (case-insensitive). Omit to target the team's default map.
|
|
178
|
+
*/
|
|
179
|
+
map?: string;
|
|
180
|
+
}
|
|
181
|
+
export interface PushResponse {
|
|
182
|
+
/** HTTP status on failure, absent for network errors. */
|
|
183
|
+
statusCode?: number;
|
|
184
|
+
success: boolean;
|
|
185
|
+
timestamp: number;
|
|
186
|
+
error?: string;
|
|
187
|
+
}
|
|
188
|
+
export declare class NocvilleClient {
|
|
189
|
+
private config;
|
|
190
|
+
private lastStatus;
|
|
191
|
+
private lastMood;
|
|
192
|
+
private lastUpdate;
|
|
193
|
+
private _links;
|
|
194
|
+
private _actions;
|
|
195
|
+
private _logBuffer;
|
|
196
|
+
private _heartbeatInterval;
|
|
197
|
+
constructor(config: NocvilleConfig);
|
|
198
|
+
/**
|
|
199
|
+
* Push a status update to the Nocville server
|
|
200
|
+
*/
|
|
201
|
+
pushStatus(update: StatusUpdate): Promise<PushResponse>;
|
|
202
|
+
/**
|
|
203
|
+
* Quick status helpers
|
|
204
|
+
*/
|
|
205
|
+
healthy(message?: string, metrics?: Record<string, number | string>): Promise<PushResponse>;
|
|
206
|
+
warning(message?: string, metrics?: Record<string, number | string>): Promise<PushResponse>;
|
|
207
|
+
critical(message?: string, metrics?: Record<string, number | string>): Promise<PushResponse>;
|
|
208
|
+
/**
|
|
209
|
+
* Get the last known status
|
|
210
|
+
*/
|
|
211
|
+
getLastStatus(): {
|
|
212
|
+
status: Status;
|
|
213
|
+
timestamp: number;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Push screen data for NPC display
|
|
217
|
+
*/
|
|
218
|
+
pushScreen(data: ScreenData): Promise<PushResponse>;
|
|
219
|
+
/**
|
|
220
|
+
* Push metrics as a multi-metric gauge layout
|
|
221
|
+
*/
|
|
222
|
+
pushGauges(metrics: Record<string, number>): Promise<PushResponse>;
|
|
223
|
+
/**
|
|
224
|
+
* Push a ticker-style screen display
|
|
225
|
+
*/
|
|
226
|
+
pushTicker(values: Array<{
|
|
227
|
+
label: string;
|
|
228
|
+
value: string;
|
|
229
|
+
trend?: 'up' | 'down' | 'flat';
|
|
230
|
+
}>): Promise<PushResponse>;
|
|
231
|
+
/**
|
|
232
|
+
* Push a log line to a rolling terminal display (keeps last 20 lines)
|
|
233
|
+
*/
|
|
234
|
+
pushLogLine(line: string): Promise<PushResponse>;
|
|
235
|
+
/**
|
|
236
|
+
* Start a heartbeat that periodically pushes the current status
|
|
237
|
+
*/
|
|
238
|
+
startHeartbeat(intervalMs?: number): void;
|
|
239
|
+
/**
|
|
240
|
+
* Stop the heartbeat interval
|
|
241
|
+
*/
|
|
242
|
+
stopHeartbeat(): void;
|
|
243
|
+
/**
|
|
244
|
+
* Set links to be included in every subsequent push
|
|
245
|
+
*/
|
|
246
|
+
setLinks(links: NPCLink[]): void;
|
|
247
|
+
/**
|
|
248
|
+
* Set actions to be included in every subsequent push
|
|
249
|
+
*/
|
|
250
|
+
setActions(actions: NPCAction[]): void;
|
|
251
|
+
/**
|
|
252
|
+
* Open an incident for this service
|
|
253
|
+
*/
|
|
254
|
+
openIncident(title: string, severity?: 'minor' | 'major' | 'critical'): Promise<{
|
|
255
|
+
id: string;
|
|
256
|
+
}>;
|
|
257
|
+
/**
|
|
258
|
+
* Resolve an incident by ID
|
|
259
|
+
*/
|
|
260
|
+
resolveIncident(incidentId: string): Promise<void>;
|
|
261
|
+
/**
|
|
262
|
+
* Add a note to an incident timeline
|
|
263
|
+
*/
|
|
264
|
+
addIncidentNote(incidentId: string, note: string): Promise<void>;
|
|
265
|
+
private moodFromStatus;
|
|
266
|
+
private makeRequest;
|
|
267
|
+
private sleep;
|
|
268
|
+
private log;
|
|
269
|
+
}
|
|
270
|
+
export default NocvilleClient;
|
|
271
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nocville Status Pusher SDK for Node.js
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { NocvilleClient } from '@nocville/node';
|
|
7
|
+
*
|
|
8
|
+
* const client = new NocvilleClient({
|
|
9
|
+
* apiKey: 'your-api-key',
|
|
10
|
+
* serviceId: 'my-service',
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* // Push a status update
|
|
14
|
+
* await client.pushStatus({
|
|
15
|
+
* status: 'healthy',
|
|
16
|
+
* mood: 'happy',
|
|
17
|
+
* message: 'All systems operational',
|
|
18
|
+
* metrics: {
|
|
19
|
+
* cpu: 45,
|
|
20
|
+
* memory: 60,
|
|
21
|
+
* requests: 1250,
|
|
22
|
+
* },
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
class StatusPushError extends Error {
|
|
27
|
+
constructor(statusCode, body) {
|
|
28
|
+
super(`HTTP ${statusCode}: ${body}`);
|
|
29
|
+
this.statusCode = statusCode;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class NocvilleClient {
|
|
33
|
+
constructor(config) {
|
|
34
|
+
this.lastStatus = 'unknown';
|
|
35
|
+
this.lastMood = 'idle';
|
|
36
|
+
this.lastUpdate = 0;
|
|
37
|
+
this._links = [];
|
|
38
|
+
this._actions = [];
|
|
39
|
+
this._logBuffer = [];
|
|
40
|
+
this._heartbeatInterval = null;
|
|
41
|
+
this.config = {
|
|
42
|
+
apiUrl: 'https://nocville.com',
|
|
43
|
+
timeout: 5000,
|
|
44
|
+
retries: 3,
|
|
45
|
+
retryDelay: 1000,
|
|
46
|
+
debug: false,
|
|
47
|
+
...config,
|
|
48
|
+
};
|
|
49
|
+
this.config.apiUrl = this.config.apiUrl.replace(/\/+$/, '');
|
|
50
|
+
if (!this.config.apiKey) {
|
|
51
|
+
throw new Error('apiKey is required');
|
|
52
|
+
}
|
|
53
|
+
if (!this.config.serviceId) {
|
|
54
|
+
throw new Error('serviceId is required');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Push a status update to the Nocville server
|
|
59
|
+
*/
|
|
60
|
+
async pushStatus(update) {
|
|
61
|
+
const payload = {
|
|
62
|
+
serviceId: this.config.serviceId,
|
|
63
|
+
status: update.status,
|
|
64
|
+
mood: update.mood || this.moodFromStatus(update.status),
|
|
65
|
+
message: update.message,
|
|
66
|
+
metrics: update.metrics,
|
|
67
|
+
};
|
|
68
|
+
if (update.agentStats !== undefined)
|
|
69
|
+
payload.agentStats = update.agentStats;
|
|
70
|
+
if (update.appearance !== undefined)
|
|
71
|
+
payload.appearance = update.appearance;
|
|
72
|
+
if (update.name !== undefined)
|
|
73
|
+
payload.name = update.name;
|
|
74
|
+
if (update.archetype !== undefined)
|
|
75
|
+
payload.archetype = update.archetype;
|
|
76
|
+
if (update.subStatus !== undefined)
|
|
77
|
+
payload.subStatus = update.subStatus;
|
|
78
|
+
if (update.screenData !== undefined)
|
|
79
|
+
payload.screenData = update.screenData;
|
|
80
|
+
if (update.alertConfig !== undefined)
|
|
81
|
+
payload.alertConfig = update.alertConfig;
|
|
82
|
+
if (update.behavior !== undefined)
|
|
83
|
+
payload.behavior = update.behavior;
|
|
84
|
+
if (update.companion !== undefined)
|
|
85
|
+
payload.companion = update.companion;
|
|
86
|
+
if (update.offline !== undefined)
|
|
87
|
+
payload.offline = update.offline;
|
|
88
|
+
if (update.map !== undefined)
|
|
89
|
+
payload.map = update.map;
|
|
90
|
+
// Merge stored links/actions with per-call overrides
|
|
91
|
+
const links = update.links || (this._links.length > 0 ? this._links : undefined);
|
|
92
|
+
const actions = update.actions || (this._actions.length > 0 ? this._actions : undefined);
|
|
93
|
+
if (links)
|
|
94
|
+
payload.links = links;
|
|
95
|
+
if (actions)
|
|
96
|
+
payload.actions = actions;
|
|
97
|
+
let lastError = null;
|
|
98
|
+
for (let attempt = 0; attempt <= this.config.retries; attempt++) {
|
|
99
|
+
try {
|
|
100
|
+
if (attempt > 0) {
|
|
101
|
+
const delay = this.config.retryDelay * Math.pow(2, attempt - 1);
|
|
102
|
+
this.log(`Retry attempt ${attempt} after ${delay}ms`);
|
|
103
|
+
await this.sleep(delay);
|
|
104
|
+
}
|
|
105
|
+
const response = await this.makeRequest(payload);
|
|
106
|
+
this.lastStatus = update.status;
|
|
107
|
+
this.lastMood = update.mood || this.moodFromStatus(update.status);
|
|
108
|
+
this.lastUpdate = Date.now();
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
timestamp: this.lastUpdate,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
lastError = error;
|
|
116
|
+
this.log(`Request failed: ${lastError.message}`);
|
|
117
|
+
if (lastError instanceof StatusPushError &&
|
|
118
|
+
[400, 401, 403, 404].includes(lastError.statusCode))
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
error: lastError?.message || 'Unknown error',
|
|
126
|
+
statusCode: lastError instanceof StatusPushError ? lastError.statusCode : undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Quick status helpers
|
|
131
|
+
*/
|
|
132
|
+
async healthy(message, metrics) {
|
|
133
|
+
return this.pushStatus({ status: 'healthy', mood: 'happy', message, metrics });
|
|
134
|
+
}
|
|
135
|
+
async warning(message, metrics) {
|
|
136
|
+
return this.pushStatus({ status: 'warning', mood: 'concerned', message, metrics });
|
|
137
|
+
}
|
|
138
|
+
async critical(message, metrics) {
|
|
139
|
+
return this.pushStatus({ status: 'critical', mood: 'stressed', message, metrics });
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get the last known status
|
|
143
|
+
*/
|
|
144
|
+
getLastStatus() {
|
|
145
|
+
return {
|
|
146
|
+
status: this.lastStatus,
|
|
147
|
+
timestamp: this.lastUpdate,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Push screen data for NPC display
|
|
152
|
+
*/
|
|
153
|
+
async pushScreen(data) {
|
|
154
|
+
return this.pushStatus({
|
|
155
|
+
status: this.lastStatus,
|
|
156
|
+
mood: this.lastMood,
|
|
157
|
+
screenData: data,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Push metrics as a multi-metric gauge layout
|
|
162
|
+
*/
|
|
163
|
+
async pushGauges(metrics) {
|
|
164
|
+
const screenData = {
|
|
165
|
+
layout: 'multi-metric',
|
|
166
|
+
values: Object.entries(metrics).map(([label, value]) => ({
|
|
167
|
+
label,
|
|
168
|
+
value: String(value),
|
|
169
|
+
})),
|
|
170
|
+
};
|
|
171
|
+
return this.pushScreen(screenData);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Push a ticker-style screen display
|
|
175
|
+
*/
|
|
176
|
+
async pushTicker(values) {
|
|
177
|
+
const screenData = {
|
|
178
|
+
layout: 'ticker',
|
|
179
|
+
values: values.map((v) => ({
|
|
180
|
+
label: v.label,
|
|
181
|
+
value: v.value,
|
|
182
|
+
trend: v.trend,
|
|
183
|
+
})),
|
|
184
|
+
};
|
|
185
|
+
return this.pushScreen(screenData);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Push a log line to a rolling terminal display (keeps last 20 lines)
|
|
189
|
+
*/
|
|
190
|
+
async pushLogLine(line) {
|
|
191
|
+
this._logBuffer.push(line);
|
|
192
|
+
if (this._logBuffer.length > 20) {
|
|
193
|
+
this._logBuffer = this._logBuffer.slice(-20);
|
|
194
|
+
}
|
|
195
|
+
const screenData = {
|
|
196
|
+
layout: 'log-terminal',
|
|
197
|
+
values: [{ label: 'log', value: this._logBuffer.join('\n') }],
|
|
198
|
+
};
|
|
199
|
+
return this.pushScreen(screenData);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Start a heartbeat that periodically pushes the current status
|
|
203
|
+
*/
|
|
204
|
+
startHeartbeat(intervalMs = 30000) {
|
|
205
|
+
this.stopHeartbeat();
|
|
206
|
+
this._heartbeatInterval = setInterval(() => {
|
|
207
|
+
this.pushStatus({
|
|
208
|
+
status: this.lastStatus,
|
|
209
|
+
mood: this.lastMood,
|
|
210
|
+
}).catch((err) => this.log(`Heartbeat push failed: ${err}`));
|
|
211
|
+
}, intervalMs);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Stop the heartbeat interval
|
|
215
|
+
*/
|
|
216
|
+
stopHeartbeat() {
|
|
217
|
+
if (this._heartbeatInterval !== null) {
|
|
218
|
+
clearInterval(this._heartbeatInterval);
|
|
219
|
+
this._heartbeatInterval = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Set links to be included in every subsequent push
|
|
224
|
+
*/
|
|
225
|
+
setLinks(links) {
|
|
226
|
+
this._links = links;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Set actions to be included in every subsequent push
|
|
230
|
+
*/
|
|
231
|
+
setActions(actions) {
|
|
232
|
+
this._actions = actions;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Open an incident for this service
|
|
236
|
+
*/
|
|
237
|
+
async openIncident(title, severity = 'critical') {
|
|
238
|
+
const url = `${this.config.apiUrl}/api/v1/teams/_/incidents/sdk`;
|
|
239
|
+
const controller = new AbortController();
|
|
240
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
241
|
+
try {
|
|
242
|
+
const response = await fetch(url, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: {
|
|
245
|
+
'Content-Type': 'application/json',
|
|
246
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
serviceId: this.config.serviceId,
|
|
250
|
+
title,
|
|
251
|
+
severity,
|
|
252
|
+
}),
|
|
253
|
+
signal: controller.signal,
|
|
254
|
+
});
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
const errorText = await response.text();
|
|
257
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
258
|
+
}
|
|
259
|
+
const result = (await response.json());
|
|
260
|
+
this.log(`Incident opened: ${result.data.id}`);
|
|
261
|
+
return { id: result.data.id };
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
clearTimeout(timeoutId);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Resolve an incident by ID
|
|
269
|
+
*/
|
|
270
|
+
async resolveIncident(incidentId) {
|
|
271
|
+
const url = `${this.config.apiUrl}/api/v1/teams/_/incidents/${incidentId}/sdk`;
|
|
272
|
+
const controller = new AbortController();
|
|
273
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
274
|
+
try {
|
|
275
|
+
const response = await fetch(url, {
|
|
276
|
+
method: 'PATCH',
|
|
277
|
+
headers: {
|
|
278
|
+
'Content-Type': 'application/json',
|
|
279
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
280
|
+
},
|
|
281
|
+
body: JSON.stringify({ status: 'resolved' }),
|
|
282
|
+
signal: controller.signal,
|
|
283
|
+
});
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
const errorText = await response.text();
|
|
286
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
287
|
+
}
|
|
288
|
+
this.log(`Incident resolved: ${incidentId}`);
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
clearTimeout(timeoutId);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Add a note to an incident timeline
|
|
296
|
+
*/
|
|
297
|
+
async addIncidentNote(incidentId, note) {
|
|
298
|
+
const url = `${this.config.apiUrl}/api/v1/teams/_/incidents/${incidentId}/events/sdk`;
|
|
299
|
+
const controller = new AbortController();
|
|
300
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
301
|
+
try {
|
|
302
|
+
const response = await fetch(url, {
|
|
303
|
+
method: 'POST',
|
|
304
|
+
headers: {
|
|
305
|
+
'Content-Type': 'application/json',
|
|
306
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
307
|
+
},
|
|
308
|
+
body: JSON.stringify({
|
|
309
|
+
type: 'note',
|
|
310
|
+
data: { note },
|
|
311
|
+
}),
|
|
312
|
+
signal: controller.signal,
|
|
313
|
+
});
|
|
314
|
+
if (!response.ok) {
|
|
315
|
+
const errorText = await response.text();
|
|
316
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
317
|
+
}
|
|
318
|
+
this.log(`Note added to incident: ${incidentId}`);
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
clearTimeout(timeoutId);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
moodFromStatus(status) {
|
|
325
|
+
switch (status) {
|
|
326
|
+
case 'healthy':
|
|
327
|
+
return 'happy';
|
|
328
|
+
case 'warning':
|
|
329
|
+
return 'concerned';
|
|
330
|
+
case 'critical':
|
|
331
|
+
return 'stressed';
|
|
332
|
+
default:
|
|
333
|
+
return 'idle';
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async makeRequest(payload) {
|
|
337
|
+
const url = `${this.config.apiUrl}/api/v1/status`;
|
|
338
|
+
const controller = new AbortController();
|
|
339
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
340
|
+
try {
|
|
341
|
+
const response = await fetch(url, {
|
|
342
|
+
method: 'POST',
|
|
343
|
+
headers: {
|
|
344
|
+
'Content-Type': 'application/json',
|
|
345
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
346
|
+
},
|
|
347
|
+
body: JSON.stringify(payload),
|
|
348
|
+
signal: controller.signal,
|
|
349
|
+
});
|
|
350
|
+
if (!response.ok) {
|
|
351
|
+
const errorText = await response.text();
|
|
352
|
+
throw new StatusPushError(response.status, errorText);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
clearTimeout(timeoutId);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
sleep(ms) {
|
|
360
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
361
|
+
}
|
|
362
|
+
log(message) {
|
|
363
|
+
if (this.config.debug) {
|
|
364
|
+
console.log(`[nocville] ${message}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Default export for convenience
|
|
369
|
+
export default NocvilleClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocville/node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Nocville Status Pusher SDK for Node.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/cjs/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"!dist/**/*.map",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "^5.3.2",
|
|
24
|
+
"vitest": "^3.2.6"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"nocville",
|
|
28
|
+
"monitoring",
|
|
29
|
+
"noc",
|
|
30
|
+
"status"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/marshmansf/nocville.git",
|
|
36
|
+
"directory": "libs/nocville-node"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20.12.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist/cjs/package.json', JSON.stringify({ type: 'commonjs' }))\"",
|
|
46
|
+
"clean": "rm -rf dist",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest"
|
|
50
|
+
}
|
|
6
51
|
}
|