@flowdot.ai/daemon 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +45 -0
- package/README.md +51 -0
- package/dist/goals/DependencyResolver.d.ts +54 -0
- package/dist/goals/DependencyResolver.js +329 -0
- package/dist/goals/ErrorRecovery.d.ts +133 -0
- package/dist/goals/ErrorRecovery.js +489 -0
- package/dist/goals/GoalApiClient.d.ts +81 -0
- package/dist/goals/GoalApiClient.js +743 -0
- package/dist/goals/GoalCache.d.ts +65 -0
- package/dist/goals/GoalCache.js +243 -0
- package/dist/goals/GoalCommsHandler.d.ts +150 -0
- package/dist/goals/GoalCommsHandler.js +378 -0
- package/dist/goals/GoalExporter.d.ts +164 -0
- package/dist/goals/GoalExporter.js +318 -0
- package/dist/goals/GoalImporter.d.ts +107 -0
- package/dist/goals/GoalImporter.js +345 -0
- package/dist/goals/GoalManager.d.ts +110 -0
- package/dist/goals/GoalManager.js +535 -0
- package/dist/goals/GoalReporter.d.ts +105 -0
- package/dist/goals/GoalReporter.js +534 -0
- package/dist/goals/GoalScheduler.d.ts +102 -0
- package/dist/goals/GoalScheduler.js +209 -0
- package/dist/goals/GoalValidator.d.ts +72 -0
- package/dist/goals/GoalValidator.js +657 -0
- package/dist/goals/MetaGoalEnforcer.d.ts +111 -0
- package/dist/goals/MetaGoalEnforcer.js +536 -0
- package/dist/goals/MilestoneBreaker.d.ts +74 -0
- package/dist/goals/MilestoneBreaker.js +348 -0
- package/dist/goals/PermissionBridge.d.ts +109 -0
- package/dist/goals/PermissionBridge.js +326 -0
- package/dist/goals/ProgressTracker.d.ts +113 -0
- package/dist/goals/ProgressTracker.js +324 -0
- package/dist/goals/ReviewScheduler.d.ts +106 -0
- package/dist/goals/ReviewScheduler.js +360 -0
- package/dist/goals/TaskExecutor.d.ts +116 -0
- package/dist/goals/TaskExecutor.js +370 -0
- package/dist/goals/TaskFeedback.d.ts +126 -0
- package/dist/goals/TaskFeedback.js +402 -0
- package/dist/goals/TaskGenerator.d.ts +75 -0
- package/dist/goals/TaskGenerator.js +329 -0
- package/dist/goals/TaskQueue.d.ts +84 -0
- package/dist/goals/TaskQueue.js +331 -0
- package/dist/goals/TaskSanitizer.d.ts +61 -0
- package/dist/goals/TaskSanitizer.js +464 -0
- package/dist/goals/errors.d.ts +116 -0
- package/dist/goals/errors.js +299 -0
- package/dist/goals/index.d.ts +24 -0
- package/dist/goals/index.js +23 -0
- package/dist/goals/types.d.ts +395 -0
- package/dist/goals/types.js +230 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/loop/DaemonIPC.d.ts +67 -0
- package/dist/loop/DaemonIPC.js +358 -0
- package/dist/loop/IntervalParser.d.ts +39 -0
- package/dist/loop/IntervalParser.js +217 -0
- package/dist/loop/LoopDaemon.d.ts +123 -0
- package/dist/loop/LoopDaemon.js +1821 -0
- package/dist/loop/LoopExecutor.d.ts +93 -0
- package/dist/loop/LoopExecutor.js +326 -0
- package/dist/loop/LoopManager.d.ts +79 -0
- package/dist/loop/LoopManager.js +476 -0
- package/dist/loop/LoopScheduler.d.ts +69 -0
- package/dist/loop/LoopScheduler.js +329 -0
- package/dist/loop/LoopStore.d.ts +57 -0
- package/dist/loop/LoopStore.js +406 -0
- package/dist/loop/LoopValidator.d.ts +55 -0
- package/dist/loop/LoopValidator.js +603 -0
- package/dist/loop/errors.d.ts +115 -0
- package/dist/loop/errors.js +312 -0
- package/dist/loop/index.d.ts +11 -0
- package/dist/loop/index.js +10 -0
- package/dist/loop/notifications/Notifier.d.ts +28 -0
- package/dist/loop/notifications/Notifier.js +78 -0
- package/dist/loop/notifications/SlackNotifier.d.ts +28 -0
- package/dist/loop/notifications/SlackNotifier.js +203 -0
- package/dist/loop/notifications/TerminalNotifier.d.ts +18 -0
- package/dist/loop/notifications/TerminalNotifier.js +72 -0
- package/dist/loop/notifications/WebhookNotifier.d.ts +24 -0
- package/dist/loop/notifications/WebhookNotifier.js +123 -0
- package/dist/loop/notifications/index.d.ts +24 -0
- package/dist/loop/notifications/index.js +109 -0
- package/dist/loop/types.d.ts +280 -0
- package/dist/loop/types.js +222 -0
- package/package.json +92 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { BaseNotifier } from './Notifier.js';
|
|
2
|
+
import { SlackError } from '../errors.js';
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
4
|
+
const DEFAULT_RETRIES = 2;
|
|
5
|
+
const RETRY_DELAY_MS = 1000;
|
|
6
|
+
const MAX_BLOCK_TEXT_LENGTH = 3000;
|
|
7
|
+
export class SlackNotifier extends BaseNotifier {
|
|
8
|
+
type = 'slack';
|
|
9
|
+
webhookUrl;
|
|
10
|
+
timeout;
|
|
11
|
+
retries;
|
|
12
|
+
channel;
|
|
13
|
+
username;
|
|
14
|
+
iconEmoji;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
super({ logger: options.logger });
|
|
17
|
+
if (!options.webhookUrl) {
|
|
18
|
+
throw new SlackError('Slack webhook URL is required');
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(options.webhookUrl);
|
|
22
|
+
if (!url.hostname.includes('slack.com') && !url.hostname.includes('hooks.slack.com')) {
|
|
23
|
+
this.logger.warn('LOOP', 'Slack webhook URL does not appear to be a Slack domain', {
|
|
24
|
+
hostname: url.hostname,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new SlackError('Invalid Slack webhook URL format');
|
|
30
|
+
}
|
|
31
|
+
this.webhookUrl = options.webhookUrl;
|
|
32
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
33
|
+
this.retries = options.retries ?? DEFAULT_RETRIES;
|
|
34
|
+
this.channel = options.channel;
|
|
35
|
+
this.username = options.username ?? 'FlowDot Loop';
|
|
36
|
+
this.iconEmoji = options.iconEmoji ?? ':robot_face:';
|
|
37
|
+
}
|
|
38
|
+
async send(payload) {
|
|
39
|
+
const slackPayload = this.buildPayload(payload);
|
|
40
|
+
let lastError = null;
|
|
41
|
+
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
42
|
+
try {
|
|
43
|
+
await this.sendRequest(slackPayload);
|
|
44
|
+
this.logNotification(payload, true);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
49
|
+
if (attempt < this.retries) {
|
|
50
|
+
this.logger.debug('LOOP', `Slack attempt ${attempt + 1} failed, retrying...`, {
|
|
51
|
+
error: lastError.message,
|
|
52
|
+
});
|
|
53
|
+
await this.delay(RETRY_DELAY_MS);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
this.handleError(lastError ?? new Error('Unknown Slack error'), payload);
|
|
58
|
+
}
|
|
59
|
+
buildPayload(notification) {
|
|
60
|
+
const title = this.formatTitle(notification);
|
|
61
|
+
const blocks = [
|
|
62
|
+
{
|
|
63
|
+
type: 'header',
|
|
64
|
+
text: {
|
|
65
|
+
type: 'plain_text',
|
|
66
|
+
text: title,
|
|
67
|
+
emoji: true,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
if (notification.message) {
|
|
72
|
+
const truncatedMessage = this.truncateForSlack(notification.message);
|
|
73
|
+
blocks.push({
|
|
74
|
+
type: 'section',
|
|
75
|
+
text: {
|
|
76
|
+
type: 'mrkdwn',
|
|
77
|
+
text: truncatedMessage,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (notification.error) {
|
|
82
|
+
blocks.push({
|
|
83
|
+
type: 'section',
|
|
84
|
+
text: {
|
|
85
|
+
type: 'mrkdwn',
|
|
86
|
+
text: `:warning: *Error:*\n\`\`\`${this.truncateForSlack(notification.error, 500)}\`\`\``,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const contextFields = [
|
|
91
|
+
{
|
|
92
|
+
type: 'mrkdwn',
|
|
93
|
+
text: `*Loop:* ${notification.loopName ?? notification.loopId.substring(0, 8)}`,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: 'mrkdwn',
|
|
97
|
+
text: `*Time:* ${notification.timestamp.toLocaleString()}`,
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
if (notification.runId) {
|
|
101
|
+
contextFields.push({
|
|
102
|
+
type: 'mrkdwn',
|
|
103
|
+
text: `*Run:* ${notification.runId.substring(0, 8)}`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (notification.status) {
|
|
107
|
+
const statusEmoji = this.getStatusEmoji(notification.status);
|
|
108
|
+
contextFields.push({
|
|
109
|
+
type: 'mrkdwn',
|
|
110
|
+
text: `*Status:* ${statusEmoji} ${notification.status}`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
blocks.push({
|
|
114
|
+
type: 'section',
|
|
115
|
+
fields: contextFields,
|
|
116
|
+
});
|
|
117
|
+
blocks.push({ type: 'divider' });
|
|
118
|
+
const payload = {
|
|
119
|
+
text: title,
|
|
120
|
+
blocks,
|
|
121
|
+
username: this.username,
|
|
122
|
+
icon_emoji: this.iconEmoji,
|
|
123
|
+
};
|
|
124
|
+
if (this.channel) {
|
|
125
|
+
payload.channel = this.channel;
|
|
126
|
+
}
|
|
127
|
+
return payload;
|
|
128
|
+
}
|
|
129
|
+
async sendRequest(payload) {
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetch(this.webhookUrl, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: {
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify(payload),
|
|
139
|
+
signal: controller.signal,
|
|
140
|
+
});
|
|
141
|
+
const responseText = await response.text();
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
throw new SlackError(`HTTP ${response.status}: ${responseText || response.statusText}`);
|
|
144
|
+
}
|
|
145
|
+
if (responseText !== 'ok' && responseText.startsWith('{')) {
|
|
146
|
+
let errorResponse = null;
|
|
147
|
+
try {
|
|
148
|
+
errorResponse = JSON.parse(responseText);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
}
|
|
152
|
+
if (errorResponse?.error) {
|
|
153
|
+
throw new SlackError(`Slack API error: ${errorResponse.error}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error instanceof SlackError) {
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
162
|
+
throw new SlackError(`Request timeout after ${this.timeout}ms`);
|
|
163
|
+
}
|
|
164
|
+
throw new SlackError(error instanceof Error ? error.message : String(error));
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
clearTimeout(timeoutId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
getStatusColor(status) {
|
|
171
|
+
switch (status) {
|
|
172
|
+
case 'success':
|
|
173
|
+
return '#36a64f';
|
|
174
|
+
case 'error':
|
|
175
|
+
return '#e01e5a';
|
|
176
|
+
case 'running':
|
|
177
|
+
return '#2eb886';
|
|
178
|
+
default:
|
|
179
|
+
return '#439fe0';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
getStatusEmoji(status) {
|
|
183
|
+
switch (status) {
|
|
184
|
+
case 'success':
|
|
185
|
+
return ':white_check_mark:';
|
|
186
|
+
case 'error':
|
|
187
|
+
return ':x:';
|
|
188
|
+
case 'running':
|
|
189
|
+
return ':arrows_counterclockwise:';
|
|
190
|
+
default:
|
|
191
|
+
return ':information_source:';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
truncateForSlack(text, maxLength = MAX_BLOCK_TEXT_LENGTH) {
|
|
195
|
+
if (text.length <= maxLength) {
|
|
196
|
+
return text;
|
|
197
|
+
}
|
|
198
|
+
return text.substring(0, maxLength - 3) + '...';
|
|
199
|
+
}
|
|
200
|
+
delay(ms) {
|
|
201
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NotifyMethod, NotificationPayload, Logger } from '../types.js';
|
|
2
|
+
import { BaseNotifier } from './Notifier.js';
|
|
3
|
+
export interface TerminalNotifierOptions {
|
|
4
|
+
sound?: boolean;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
logger?: Logger;
|
|
7
|
+
}
|
|
8
|
+
export declare class TerminalNotifier extends BaseNotifier {
|
|
9
|
+
readonly type: NotifyMethod;
|
|
10
|
+
private notifier;
|
|
11
|
+
private readonly sound;
|
|
12
|
+
private readonly timeout;
|
|
13
|
+
constructor(options?: TerminalNotifierOptions);
|
|
14
|
+
send(payload: NotificationPayload): Promise<void>;
|
|
15
|
+
private getNotifier;
|
|
16
|
+
private sendNotification;
|
|
17
|
+
protected formatMessage(payload: NotificationPayload): string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { BaseNotifier } from './Notifier.js';
|
|
2
|
+
export class TerminalNotifier extends BaseNotifier {
|
|
3
|
+
type = 'terminal';
|
|
4
|
+
notifier = null;
|
|
5
|
+
sound;
|
|
6
|
+
timeout;
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
super({ logger: options.logger });
|
|
9
|
+
this.sound = options.sound ?? true;
|
|
10
|
+
this.timeout = options.timeout ?? 10;
|
|
11
|
+
}
|
|
12
|
+
async send(payload) {
|
|
13
|
+
try {
|
|
14
|
+
const notifier = await this.getNotifier();
|
|
15
|
+
const title = this.formatTitle(payload);
|
|
16
|
+
const message = this.formatMessage(payload);
|
|
17
|
+
await this.sendNotification(notifier, {
|
|
18
|
+
title,
|
|
19
|
+
message,
|
|
20
|
+
sound: this.sound,
|
|
21
|
+
timeout: this.timeout,
|
|
22
|
+
wait: false,
|
|
23
|
+
});
|
|
24
|
+
this.logNotification(payload, true);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
this.handleError(error, payload);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async getNotifier() {
|
|
31
|
+
if (this.notifier) {
|
|
32
|
+
return this.notifier;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const nodeNotifier = await import('node-notifier');
|
|
36
|
+
this.notifier = nodeNotifier.default || nodeNotifier;
|
|
37
|
+
return this.notifier;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error('node-notifier is required for terminal notifications. ' +
|
|
41
|
+
'Install it with: npm install node-notifier');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
sendNotification(notifier, options) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
notifier.notify(options, (err, _response) => {
|
|
47
|
+
if (err) {
|
|
48
|
+
reject(err);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
resolve();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
formatMessage(payload) {
|
|
57
|
+
const parts = [];
|
|
58
|
+
if (payload.message) {
|
|
59
|
+
const maxLength = 200;
|
|
60
|
+
if (payload.message.length > maxLength) {
|
|
61
|
+
parts.push(payload.message.substring(0, maxLength - 3) + '...');
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
parts.push(payload.message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (payload.error) {
|
|
68
|
+
parts.push(`\nError occurred`);
|
|
69
|
+
}
|
|
70
|
+
return parts.join('');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NotifyMethod, NotificationPayload, LoopWebhookConfig, Logger } from '../types.js';
|
|
2
|
+
import { BaseNotifier } from './Notifier.js';
|
|
3
|
+
export interface WebhookNotifierOptions {
|
|
4
|
+
url: string;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
retries?: number;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
includeErrorDetails?: boolean;
|
|
9
|
+
logger?: Logger;
|
|
10
|
+
}
|
|
11
|
+
export declare class WebhookNotifier extends BaseNotifier {
|
|
12
|
+
readonly type: NotifyMethod;
|
|
13
|
+
private readonly url;
|
|
14
|
+
private readonly timeout;
|
|
15
|
+
private readonly retries;
|
|
16
|
+
private readonly headers;
|
|
17
|
+
private readonly includeErrorDetails;
|
|
18
|
+
constructor(options: WebhookNotifierOptions);
|
|
19
|
+
static fromConfig(config: LoopWebhookConfig, urlOverride?: string | null, logger?: Logger): WebhookNotifier | null;
|
|
20
|
+
send(payload: NotificationPayload): Promise<void>;
|
|
21
|
+
private buildPayload;
|
|
22
|
+
private sendRequest;
|
|
23
|
+
private delay;
|
|
24
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { BaseNotifier } from './Notifier.js';
|
|
2
|
+
import { WebhookError } from '../errors.js';
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
4
|
+
const DEFAULT_RETRIES = 2;
|
|
5
|
+
const RETRY_DELAY_MS = 1000;
|
|
6
|
+
export class WebhookNotifier extends BaseNotifier {
|
|
7
|
+
type = 'webhook';
|
|
8
|
+
url;
|
|
9
|
+
timeout;
|
|
10
|
+
retries;
|
|
11
|
+
headers;
|
|
12
|
+
includeErrorDetails;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
super({ logger: options.logger });
|
|
15
|
+
if (!options.url) {
|
|
16
|
+
throw new WebhookError('', null, 'Webhook URL is required');
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
new URL(options.url);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new WebhookError(options.url, null, 'Invalid webhook URL format');
|
|
23
|
+
}
|
|
24
|
+
this.url = options.url;
|
|
25
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
26
|
+
this.retries = options.retries ?? DEFAULT_RETRIES;
|
|
27
|
+
this.headers = {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'User-Agent': 'FlowDot-Daemon/1.0',
|
|
30
|
+
...options.headers,
|
|
31
|
+
};
|
|
32
|
+
this.includeErrorDetails = options.includeErrorDetails ?? true;
|
|
33
|
+
}
|
|
34
|
+
static fromConfig(config, urlOverride, logger) {
|
|
35
|
+
const url = urlOverride ?? config.defaultUrl;
|
|
36
|
+
if (!url) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return new WebhookNotifier({
|
|
40
|
+
url,
|
|
41
|
+
timeout: config.timeout,
|
|
42
|
+
retries: config.retries,
|
|
43
|
+
logger,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async send(payload) {
|
|
47
|
+
const webhookPayload = this.buildPayload(payload);
|
|
48
|
+
let lastError = null;
|
|
49
|
+
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
50
|
+
try {
|
|
51
|
+
await this.sendRequest(webhookPayload);
|
|
52
|
+
this.logNotification(payload, true);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
57
|
+
if (attempt < this.retries) {
|
|
58
|
+
this.logger.debug('LOOP', `Webhook attempt ${attempt + 1} failed, retrying...`, {
|
|
59
|
+
url: this.url,
|
|
60
|
+
error: lastError.message,
|
|
61
|
+
});
|
|
62
|
+
await this.delay(RETRY_DELAY_MS);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this.handleError(lastError ?? new Error('Unknown webhook error'), payload);
|
|
67
|
+
}
|
|
68
|
+
buildPayload(notification) {
|
|
69
|
+
const payload = {
|
|
70
|
+
event: 'loop_notification',
|
|
71
|
+
timestamp: notification.timestamp.toISOString(),
|
|
72
|
+
loop: {
|
|
73
|
+
id: notification.loopId,
|
|
74
|
+
name: notification.loopName,
|
|
75
|
+
},
|
|
76
|
+
notification: {
|
|
77
|
+
title: this.formatTitle(notification),
|
|
78
|
+
message: notification.message,
|
|
79
|
+
status: notification.status,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
if (notification.runId) {
|
|
83
|
+
payload.run = {
|
|
84
|
+
id: notification.runId,
|
|
85
|
+
status: notification.status,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (this.includeErrorDetails && notification.error) {
|
|
89
|
+
payload.notification.error = notification.error;
|
|
90
|
+
}
|
|
91
|
+
return payload;
|
|
92
|
+
}
|
|
93
|
+
async sendRequest(payload) {
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(this.url, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: this.headers,
|
|
100
|
+
body: JSON.stringify(payload),
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
});
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
throw new WebhookError(this.url, response.status, `HTTP ${response.status}: ${response.statusText}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error instanceof WebhookError) {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
112
|
+
throw new WebhookError(this.url, null, `Request timeout after ${this.timeout}ms`);
|
|
113
|
+
}
|
|
114
|
+
throw new WebhookError(this.url, null, error instanceof Error ? error.message : String(error));
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
clearTimeout(timeoutId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
delay(ms) {
|
|
121
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NotifyMethod, Notifier, LoopWebhookConfig, Logger } from '../types.js';
|
|
2
|
+
import { BaseNotifier, NullNotifier, MAX_MESSAGE_LENGTH, MAX_TITLE_LENGTH, type NotifierConfig } from './Notifier.js';
|
|
3
|
+
import { TerminalNotifier, type TerminalNotifierOptions } from './TerminalNotifier.js';
|
|
4
|
+
import { WebhookNotifier, type WebhookNotifierOptions } from './WebhookNotifier.js';
|
|
5
|
+
import { SlackNotifier, type SlackNotifierOptions } from './SlackNotifier.js';
|
|
6
|
+
export { BaseNotifier, NullNotifier, MAX_MESSAGE_LENGTH, MAX_TITLE_LENGTH, type NotifierConfig, };
|
|
7
|
+
export { TerminalNotifier, type TerminalNotifierOptions };
|
|
8
|
+
export { WebhookNotifier, type WebhookNotifierOptions, };
|
|
9
|
+
export { SlackNotifier, type SlackNotifierOptions, };
|
|
10
|
+
export interface NotifierFactoryConfig {
|
|
11
|
+
webhooks?: LoopWebhookConfig;
|
|
12
|
+
webhookUrl?: string | null;
|
|
13
|
+
slackWebhookUrl?: string | null;
|
|
14
|
+
terminal?: {
|
|
15
|
+
sound?: boolean;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
};
|
|
18
|
+
logger?: Logger;
|
|
19
|
+
}
|
|
20
|
+
export declare function createNotifier(method: NotifyMethod, config?: NotifierFactoryConfig): Promise<Notifier>;
|
|
21
|
+
export declare function createNotifierSync(method: Exclude<NotifyMethod, 'terminal'>, config?: NotifierFactoryConfig): Notifier;
|
|
22
|
+
export declare function createNotifiers(methods: NotifyMethod[], config?: NotifierFactoryConfig): Promise<Notifier[]>;
|
|
23
|
+
export declare function isValidNotifyMethod(method: string): method is NotifyMethod;
|
|
24
|
+
export declare function getAvailableNotifyMethods(): NotifyMethod[];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { BaseNotifier, NullNotifier, MAX_MESSAGE_LENGTH, MAX_TITLE_LENGTH, } from './Notifier.js';
|
|
2
|
+
import { TerminalNotifier } from './TerminalNotifier.js';
|
|
3
|
+
import { WebhookNotifier, } from './WebhookNotifier.js';
|
|
4
|
+
import { SlackNotifier, } from './SlackNotifier.js';
|
|
5
|
+
export { BaseNotifier, NullNotifier, MAX_MESSAGE_LENGTH, MAX_TITLE_LENGTH, };
|
|
6
|
+
export { TerminalNotifier };
|
|
7
|
+
export { WebhookNotifier, };
|
|
8
|
+
export { SlackNotifier, };
|
|
9
|
+
let TerminalNotifierClass = null;
|
|
10
|
+
async function getTerminalNotifierClass() {
|
|
11
|
+
if (TerminalNotifierClass) {
|
|
12
|
+
return TerminalNotifierClass;
|
|
13
|
+
}
|
|
14
|
+
const module = await import('./TerminalNotifier.js');
|
|
15
|
+
TerminalNotifierClass = module.TerminalNotifier;
|
|
16
|
+
return TerminalNotifierClass;
|
|
17
|
+
}
|
|
18
|
+
export async function createNotifier(method, config = {}) {
|
|
19
|
+
const { NullNotifier } = await import('./Notifier.js');
|
|
20
|
+
switch (method) {
|
|
21
|
+
case 'none':
|
|
22
|
+
return new NullNotifier();
|
|
23
|
+
case 'terminal': {
|
|
24
|
+
const TerminalNotifier = await getTerminalNotifierClass();
|
|
25
|
+
return new TerminalNotifier({
|
|
26
|
+
...config.terminal,
|
|
27
|
+
logger: config.logger,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
case 'webhook': {
|
|
31
|
+
const { WebhookNotifier } = await import('./WebhookNotifier.js');
|
|
32
|
+
const url = config.webhookUrl ?? config.webhooks?.defaultUrl;
|
|
33
|
+
if (!url) {
|
|
34
|
+
throw new Error('Webhook URL is required for webhook notifications. ' +
|
|
35
|
+
'Set --webhook-url or configure loops.webhooks.defaultUrl');
|
|
36
|
+
}
|
|
37
|
+
return new WebhookNotifier({
|
|
38
|
+
url,
|
|
39
|
+
timeout: config.webhooks?.timeout,
|
|
40
|
+
retries: config.webhooks?.retries,
|
|
41
|
+
logger: config.logger,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
case 'slack': {
|
|
45
|
+
const { SlackNotifier } = await import('./SlackNotifier.js');
|
|
46
|
+
if (!config.slackWebhookUrl) {
|
|
47
|
+
throw new Error('Slack webhook URL is required for Slack notifications. ' +
|
|
48
|
+
'Configure loops.slack.webhookUrl or set --slack-webhook-url');
|
|
49
|
+
}
|
|
50
|
+
return new SlackNotifier({
|
|
51
|
+
webhookUrl: config.slackWebhookUrl,
|
|
52
|
+
timeout: config.webhooks?.timeout,
|
|
53
|
+
retries: config.webhooks?.retries,
|
|
54
|
+
logger: config.logger,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
default: {
|
|
58
|
+
const _exhaustive = method;
|
|
59
|
+
throw new Error(`Unknown notification method: ${method}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function createNotifierSync(method, config = {}) {
|
|
64
|
+
switch (method) {
|
|
65
|
+
case 'none':
|
|
66
|
+
return new NullNotifier();
|
|
67
|
+
case 'webhook': {
|
|
68
|
+
const url = config.webhookUrl ?? config.webhooks?.defaultUrl;
|
|
69
|
+
if (!url) {
|
|
70
|
+
throw new Error('Webhook URL is required for webhook notifications.');
|
|
71
|
+
}
|
|
72
|
+
return new WebhookNotifier({
|
|
73
|
+
url,
|
|
74
|
+
timeout: config.webhooks?.timeout,
|
|
75
|
+
retries: config.webhooks?.retries,
|
|
76
|
+
logger: config.logger,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
case 'slack': {
|
|
80
|
+
if (!config.slackWebhookUrl) {
|
|
81
|
+
throw new Error('Slack webhook URL is required for Slack notifications.');
|
|
82
|
+
}
|
|
83
|
+
return new SlackNotifier({
|
|
84
|
+
webhookUrl: config.slackWebhookUrl,
|
|
85
|
+
timeout: config.webhooks?.timeout,
|
|
86
|
+
retries: config.webhooks?.retries,
|
|
87
|
+
logger: config.logger,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
default: {
|
|
91
|
+
const _exhaustive = method;
|
|
92
|
+
throw new Error(`Unknown notification method: ${method}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function createNotifiers(methods, config = {}) {
|
|
97
|
+
const uniqueMethods = [...new Set(methods.filter((m) => m !== 'none'))];
|
|
98
|
+
if (uniqueMethods.length === 0) {
|
|
99
|
+
const { NullNotifier } = await import('./Notifier.js');
|
|
100
|
+
return [new NullNotifier()];
|
|
101
|
+
}
|
|
102
|
+
return Promise.all(uniqueMethods.map((method) => createNotifier(method, config)));
|
|
103
|
+
}
|
|
104
|
+
export function isValidNotifyMethod(method) {
|
|
105
|
+
return ['none', 'terminal', 'webhook', 'slack'].includes(method);
|
|
106
|
+
}
|
|
107
|
+
export function getAvailableNotifyMethods() {
|
|
108
|
+
return ['none', 'terminal', 'webhook', 'slack'];
|
|
109
|
+
}
|