pi-harness-runtime 0.3.2-beta.2 → 0.4.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/CHANGELOG.md +18 -4
- package/README.md +20 -0
- package/harness/auto-compact.ts +326 -0
- package/harness/e2e/index.ts +44 -0
- package/harness/e2e/playwright-runner.ts +92 -53
- package/harness/e2e/test-engine.ts +9 -13
- package/harness/index.ts +105 -0
- package/harness/notification-events.ts +299 -0
- package/harness/output-limit-handler.ts +316 -0
- package/harness/partial-recovery.ts +407 -0
- package/package.json +5 -2
- package/packages/auth/src/minimax-browser-auth.ts +830 -180
- package/packages/auth/src/run-minimax-auth.ts +55 -7
- package/packages/notification/adapters/email-adapter.ts +126 -0
- package/packages/notification/adapters/ntfy-adapter.ts +100 -0
- package/packages/notification/adapters/telegram-adapter.ts +107 -0
- package/packages/notification/adapters/webhook-adapter.ts +75 -0
- package/packages/notification/base-adapter.ts +75 -0
- package/packages/notification/index.ts +51 -0
- package/packages/notification/notification-center.ts +286 -0
- package/packages/notification/types.ts +89 -0
- package/packages/providers/adapters.ts +102 -1
- package/packages/types/src/runtime-types.ts +3 -1
- package/skills/harness-runtime/SKILL.md +5 -1
|
@@ -22,7 +22,9 @@ import {
|
|
|
22
22
|
checkAuthStatus,
|
|
23
23
|
getProfileDir,
|
|
24
24
|
getStatusPath,
|
|
25
|
-
|
|
25
|
+
getLiveSessionPath,
|
|
26
|
+
startPersistentBrowserDaemon,
|
|
27
|
+
stopPersistentBrowserDaemon,
|
|
26
28
|
} from "./minimax-browser-auth.js";
|
|
27
29
|
|
|
28
30
|
async function main() {
|
|
@@ -48,7 +50,9 @@ async function main() {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
if (command === "scrape" || command === "usage") {
|
|
51
|
-
console.log(
|
|
53
|
+
console.log(
|
|
54
|
+
"Command: scrape (prefers live browser, falls back to saved profile)",
|
|
55
|
+
);
|
|
52
56
|
console.log("");
|
|
53
57
|
const status = await scrapeWithExistingProfile();
|
|
54
58
|
console.log("");
|
|
@@ -57,7 +61,48 @@ async function main() {
|
|
|
57
61
|
return;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
if (command === "
|
|
64
|
+
if (command === "open" || command === "daemon") {
|
|
65
|
+
console.log("Command: open (live browser daemon mode)");
|
|
66
|
+
console.log("");
|
|
67
|
+
console.log("This will:");
|
|
68
|
+
console.log("1. Launch a real Chrome window with the MiniMax profile");
|
|
69
|
+
console.log("2. Keep that browser session running for unattended scrapes");
|
|
70
|
+
console.log("3. Let you sign in manually once and leave the window open");
|
|
71
|
+
console.log("");
|
|
72
|
+
console.log("Profile dir:", getProfileDir());
|
|
73
|
+
console.log("");
|
|
74
|
+
|
|
75
|
+
const session = await startPersistentBrowserDaemon();
|
|
76
|
+
console.log("Live browser session:");
|
|
77
|
+
console.log(JSON.stringify(session, null, 2));
|
|
78
|
+
console.log("");
|
|
79
|
+
console.log("Next:");
|
|
80
|
+
console.log("- Sign in inside that Chrome window if needed");
|
|
81
|
+
console.log("- Leave the MiniMax Chrome window open overnight");
|
|
82
|
+
console.log(
|
|
83
|
+
"- Later, run: bun packages/auth/src/run-minimax-auth.ts scrape",
|
|
84
|
+
);
|
|
85
|
+
console.log(
|
|
86
|
+
"- To stop it, run: bun packages/auth/src/run-minimax-auth.ts stop",
|
|
87
|
+
);
|
|
88
|
+
console.log("Live session file:", getLiveSessionPath());
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (command === "stop" || command === "close") {
|
|
93
|
+
console.log("Command: stop");
|
|
94
|
+
console.log("");
|
|
95
|
+
const stopped = await stopPersistentBrowserDaemon();
|
|
96
|
+
console.log(
|
|
97
|
+
stopped
|
|
98
|
+
? "✅ Stopped MiniMax live browser session"
|
|
99
|
+
: "ℹ️ No active MiniMax live browser session was found",
|
|
100
|
+
);
|
|
101
|
+
console.log("Live session file:", getLiveSessionPath());
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (command === "auth" || command === "login") {
|
|
61
106
|
console.log("Command: auth (persistent browser mode)");
|
|
62
107
|
console.log("");
|
|
63
108
|
console.log("This will:");
|
|
@@ -78,16 +123,19 @@ async function main() {
|
|
|
78
123
|
|
|
79
124
|
console.log("Usage:");
|
|
80
125
|
console.log(
|
|
81
|
-
" bun packages/auth/src/run-minimax-auth.ts auth #
|
|
126
|
+
" bun packages/auth/src/run-minimax-auth.ts auth # Manual login + confirmation",
|
|
127
|
+
);
|
|
128
|
+
console.log(
|
|
129
|
+
" bun packages/auth/src/run-minimax-auth.ts open # Start keep-open browser daemon",
|
|
82
130
|
);
|
|
83
131
|
console.log(
|
|
84
|
-
" bun packages/auth/src/run-minimax-auth.ts scrape
|
|
132
|
+
" bun packages/auth/src/run-minimax-auth.ts scrape # Scrape usage (prefers live browser)",
|
|
85
133
|
);
|
|
86
134
|
console.log(
|
|
87
|
-
" bun packages/auth/src/run-minimax-auth.ts
|
|
135
|
+
" bun packages/auth/src/run-minimax-auth.ts stop # Stop live browser daemon",
|
|
88
136
|
);
|
|
89
137
|
console.log(
|
|
90
|
-
" bun packages/auth/src/run-minimax-auth.ts
|
|
138
|
+
" bun packages/auth/src/run-minimax-auth.ts check # Check status",
|
|
91
139
|
);
|
|
92
140
|
console.log("");
|
|
93
141
|
console.log("Security:");
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Adapter — RFC-0022
|
|
3
|
+
*
|
|
4
|
+
* Sends notifications via SMTP.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
NotificationPayload,
|
|
9
|
+
NotificationResult,
|
|
10
|
+
EmailConfig,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { BaseChannelAdapter } from "../base-adapter.js";
|
|
13
|
+
|
|
14
|
+
// Simple SMTP client using built-in net module
|
|
15
|
+
async function sendSmtpEmail(
|
|
16
|
+
host: string,
|
|
17
|
+
port: number,
|
|
18
|
+
user: string,
|
|
19
|
+
password: string,
|
|
20
|
+
from: string,
|
|
21
|
+
to: string[],
|
|
22
|
+
subject: string,
|
|
23
|
+
body: string,
|
|
24
|
+
tls: boolean = false,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
// For a production implementation, you'd use a proper SMTP library
|
|
27
|
+
// This is a placeholder that logs the email
|
|
28
|
+
console.log(`[Email] Would send to ${to.join(", ")}`);
|
|
29
|
+
console.log(`[Email] Subject: ${subject}`);
|
|
30
|
+
console.log(`[Email] Body: ${body}`);
|
|
31
|
+
|
|
32
|
+
// In practice, you'd use something like:
|
|
33
|
+
// import { createClient } from "nodemailer";
|
|
34
|
+
// const transporter = nodemailer.createTransport({ ... });
|
|
35
|
+
// await transporter.sendMail({ ... });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class EmailAdapter extends BaseChannelAdapter {
|
|
39
|
+
readonly id = "email";
|
|
40
|
+
readonly type = "email";
|
|
41
|
+
|
|
42
|
+
constructor(config: EmailConfig) {
|
|
43
|
+
super({ id: "email", type: "email", enabled: true, config });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async initialize(): Promise<boolean> {
|
|
47
|
+
// Verify SMTP connection
|
|
48
|
+
try {
|
|
49
|
+
const cfg = this.config.config as EmailConfig;
|
|
50
|
+
// In production, verify SMTP credentials
|
|
51
|
+
return Boolean(
|
|
52
|
+
cfg.smtpHost && cfg.smtpUser && cfg.from && cfg.to.length > 0,
|
|
53
|
+
);
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async send(payload: NotificationPayload): Promise<NotificationResult> {
|
|
60
|
+
try {
|
|
61
|
+
const cfg = this.config.config as EmailConfig;
|
|
62
|
+
const subject = `${this.getEmoji(payload.event)} ${payload.title}`;
|
|
63
|
+
const body = this.formatBody(payload);
|
|
64
|
+
|
|
65
|
+
await sendSmtpEmail(
|
|
66
|
+
cfg.smtpHost,
|
|
67
|
+
cfg.smtpPort,
|
|
68
|
+
cfg.smtpUser,
|
|
69
|
+
cfg.smtpPassword,
|
|
70
|
+
cfg.from,
|
|
71
|
+
cfg.to,
|
|
72
|
+
subject,
|
|
73
|
+
body,
|
|
74
|
+
cfg.tls,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return { success: true, channel: this.id };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
channel: this.id,
|
|
82
|
+
error: String(error),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private formatBody(payload: NotificationPayload): string {
|
|
88
|
+
const lines = [
|
|
89
|
+
payload.message,
|
|
90
|
+
"",
|
|
91
|
+
"---",
|
|
92
|
+
`Event: ${payload.event}`,
|
|
93
|
+
`Time: ${payload.timestamp}`,
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
if (payload.details?.taskTitle) {
|
|
97
|
+
lines.push(`Task: ${payload.details.taskTitle}`);
|
|
98
|
+
}
|
|
99
|
+
if (payload.details?.jobId) {
|
|
100
|
+
lines.push(`Job ID: ${payload.details.jobId}`);
|
|
101
|
+
}
|
|
102
|
+
if (payload.details?.error) {
|
|
103
|
+
lines.push("", `Error: ${payload.details.error}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private getEmoji(event: NotificationPayload["event"]): string {
|
|
110
|
+
const map: Record<string, string> = {
|
|
111
|
+
JobStarted: "[🚀]",
|
|
112
|
+
TaskCompleted: "[✅]",
|
|
113
|
+
TaskFailed: "[❌]",
|
|
114
|
+
QuotaPaused: "[⏸]",
|
|
115
|
+
ResumeScheduled: "[▶]",
|
|
116
|
+
ContextCompacted: "[📦]",
|
|
117
|
+
OutputLimitContinued: "[🔄]",
|
|
118
|
+
E2EFailed: "[🧪]",
|
|
119
|
+
HumanReviewNeeded: "[👤]",
|
|
120
|
+
ReadyForClient: "[🎉]",
|
|
121
|
+
JobCancelled: "[🚫]",
|
|
122
|
+
Error: "[⚠]",
|
|
123
|
+
};
|
|
124
|
+
return map[event] ?? "[📢]";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ntfy Adapter — RFC-0022
|
|
3
|
+
*
|
|
4
|
+
* Sends notifications via ntfy.sh (or self-hosted ntfy server).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
NotificationPayload,
|
|
9
|
+
NotificationResult,
|
|
10
|
+
NtfyConfig,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { BaseChannelAdapter } from "../base-adapter.js";
|
|
13
|
+
|
|
14
|
+
export class NtfyAdapter extends BaseChannelAdapter {
|
|
15
|
+
readonly id = "ntfy";
|
|
16
|
+
readonly type = "ntfy";
|
|
17
|
+
|
|
18
|
+
constructor(config: NtfyConfig) {
|
|
19
|
+
super({ id: "ntfy", type: "ntfy", enabled: true, config });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async initialize(): Promise<boolean> {
|
|
23
|
+
// Ntfy doesn't require initialization; it's fire-and-forget
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async send(payload: NotificationPayload): Promise<NotificationResult> {
|
|
28
|
+
try {
|
|
29
|
+
const cfg = this.config.config as NtfyConfig;
|
|
30
|
+
const message = this.formatMessage(payload);
|
|
31
|
+
const headers: Record<string, string> = {
|
|
32
|
+
"Content-Type": "text/plain",
|
|
33
|
+
Title: payload.title,
|
|
34
|
+
Tags: this.getTags(payload.event),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Add auth if configured
|
|
38
|
+
if (cfg.authToken) {
|
|
39
|
+
headers["Authorization"] = `Bearer ${cfg.authToken}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const response = await fetch(`${cfg.server}/${cfg.topic}`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers,
|
|
45
|
+
body: message,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const error = await response.text();
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
channel: this.id,
|
|
53
|
+
error: `Ntfy error: ${error}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { success: true, channel: this.id };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
channel: this.id,
|
|
62
|
+
error: String(error),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private formatMessage(payload: NotificationPayload): string {
|
|
68
|
+
const lines = [payload.message];
|
|
69
|
+
|
|
70
|
+
if (payload.details?.taskTitle) {
|
|
71
|
+
lines.push(`\nTask: ${payload.details.taskTitle}`);
|
|
72
|
+
}
|
|
73
|
+
if (payload.details?.jobId) {
|
|
74
|
+
lines.push(`Job: ${payload.details.jobId}`);
|
|
75
|
+
}
|
|
76
|
+
if (payload.details?.error) {
|
|
77
|
+
lines.push(`\nError: ${payload.details.error}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return lines.join("");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private getTags(event: NotificationPayload["event"]): string {
|
|
84
|
+
const map: Record<string, string> = {
|
|
85
|
+
JobStarted: "rocket",
|
|
86
|
+
TaskCompleted: "white_check_mark",
|
|
87
|
+
TaskFailed: "x",
|
|
88
|
+
QuotaPaused: "pause_button",
|
|
89
|
+
ResumeScheduled: "play_button",
|
|
90
|
+
ContextCompacted: "package",
|
|
91
|
+
OutputLimitContinued: "repeat",
|
|
92
|
+
E2EFailed: "test_tube",
|
|
93
|
+
HumanReviewNeeded: "bust_in_silhouette",
|
|
94
|
+
ReadyForClient: "tada",
|
|
95
|
+
JobCancelled: "no_entry",
|
|
96
|
+
Error: "warning",
|
|
97
|
+
};
|
|
98
|
+
return map[event] ?? "bell";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Adapter — RFC-0022
|
|
3
|
+
*
|
|
4
|
+
* Sends notifications via Telegram Bot API.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
NotificationPayload,
|
|
9
|
+
NotificationResult,
|
|
10
|
+
TelegramConfig,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { BaseChannelAdapter } from "../base-adapter.js";
|
|
13
|
+
|
|
14
|
+
export class TelegramAdapter extends BaseChannelAdapter {
|
|
15
|
+
readonly id = "telegram";
|
|
16
|
+
readonly type = "telegram";
|
|
17
|
+
|
|
18
|
+
constructor(config: TelegramConfig) {
|
|
19
|
+
super({ id: "telegram", type: "telegram", enabled: true, config });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async initialize(): Promise<boolean> {
|
|
23
|
+
try {
|
|
24
|
+
const cfg = this.config.config as TelegramConfig;
|
|
25
|
+
// Verify bot token by calling getMe
|
|
26
|
+
const response = await fetch(
|
|
27
|
+
`https://api.telegram.org/bot${cfg.botToken}/getMe`,
|
|
28
|
+
);
|
|
29
|
+
return response.ok;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async send(payload: NotificationPayload): Promise<NotificationResult> {
|
|
36
|
+
try {
|
|
37
|
+
const cfg = this.config.config as TelegramConfig;
|
|
38
|
+
const message = this.formatMessage(payload);
|
|
39
|
+
|
|
40
|
+
const response = await fetch(
|
|
41
|
+
`https://api.telegram.org/bot${cfg.botToken}/sendMessage`,
|
|
42
|
+
{
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
chat_id: cfg.chatId,
|
|
47
|
+
text: message,
|
|
48
|
+
parse_mode: cfg.parseMode ?? "MarkdownV2",
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
const error = await response.text();
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
channel: this.id,
|
|
58
|
+
error: `Telegram API error: ${error}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { success: true, channel: this.id };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
channel: this.id,
|
|
67
|
+
error: String(error),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private formatMessage(payload: NotificationPayload): string {
|
|
73
|
+
const emoji = this.getEmoji(payload.event);
|
|
74
|
+
const title = `${emoji} ${payload.title}`;
|
|
75
|
+
const lines = [title, "", payload.message];
|
|
76
|
+
|
|
77
|
+
if (payload.details?.taskTitle) {
|
|
78
|
+
lines.push("", `Task: ${payload.details.taskTitle}`);
|
|
79
|
+
}
|
|
80
|
+
if (payload.details?.jobId) {
|
|
81
|
+
lines.push(`Job: ${payload.details.jobId}`);
|
|
82
|
+
}
|
|
83
|
+
if (payload.details?.error) {
|
|
84
|
+
lines.push("", `Error: ${payload.details.error}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return lines.filter(Boolean).join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private getEmoji(event: NotificationPayload["event"]): string {
|
|
91
|
+
const map: Record<string, string> = {
|
|
92
|
+
JobStarted: "🚀",
|
|
93
|
+
TaskCompleted: "✅",
|
|
94
|
+
TaskFailed: "❌",
|
|
95
|
+
QuotaPaused: "⏸️",
|
|
96
|
+
ResumeScheduled: "▶️",
|
|
97
|
+
ContextCompacted: "📦",
|
|
98
|
+
OutputLimitContinued: "🔄",
|
|
99
|
+
E2EFailed: "🧪",
|
|
100
|
+
HumanReviewNeeded: "👤",
|
|
101
|
+
ReadyForClient: "🎉",
|
|
102
|
+
JobCancelled: "🚫",
|
|
103
|
+
Error: "⚠️",
|
|
104
|
+
};
|
|
105
|
+
return map[event] ?? "📢";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Adapter — RFC-0022
|
|
3
|
+
*
|
|
4
|
+
* Sends notifications to a generic webhook endpoint.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
NotificationPayload,
|
|
9
|
+
NotificationResult,
|
|
10
|
+
WebhookConfig,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { BaseChannelAdapter } from "../base-adapter.js";
|
|
13
|
+
|
|
14
|
+
export class WebhookAdapter extends BaseChannelAdapter {
|
|
15
|
+
readonly id = "webhook";
|
|
16
|
+
readonly type = "webhook";
|
|
17
|
+
|
|
18
|
+
constructor(config: WebhookConfig) {
|
|
19
|
+
super({ id: "webhook", type: "webhook", enabled: true, config });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async initialize(): Promise<boolean> {
|
|
23
|
+
try {
|
|
24
|
+
const cfg = this.config.config as WebhookConfig;
|
|
25
|
+
return Boolean(cfg.url && cfg.url.startsWith("http"));
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async send(payload: NotificationPayload): Promise<NotificationResult> {
|
|
32
|
+
try {
|
|
33
|
+
const cfg = this.config.config as WebhookConfig;
|
|
34
|
+
const headers: Record<string, string> = {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
...(cfg.headers ?? {}),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Add auth token if configured
|
|
40
|
+
if (cfg.authToken) {
|
|
41
|
+
headers["Authorization"] = `Bearer ${cfg.authToken}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const response = await fetch(cfg.url, {
|
|
45
|
+
method: cfg.method ?? "POST",
|
|
46
|
+
headers,
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
event: payload.event,
|
|
49
|
+
jobId: payload.jobId,
|
|
50
|
+
timestamp: payload.timestamp,
|
|
51
|
+
title: payload.title,
|
|
52
|
+
message: payload.message,
|
|
53
|
+
details: payload.details,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const error = await response.text();
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
channel: this.id,
|
|
62
|
+
error: `Webhook error ${response.status}: ${error}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { success: true, channel: this.id };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
channel: this.id,
|
|
71
|
+
error: String(error),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Base Adapter — RFC-0022
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class for all notification channel adapters.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
NotificationPayload,
|
|
9
|
+
NotificationChannelConfig,
|
|
10
|
+
NotificationResult,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
|
|
13
|
+
export interface ChannelAdapter {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly type: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the adapter (e.g., verify credentials)
|
|
19
|
+
*/
|
|
20
|
+
initialize(): Promise<boolean>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Send a notification
|
|
24
|
+
*/
|
|
25
|
+
send(payload: NotificationPayload): Promise<NotificationResult>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if the adapter is properly configured
|
|
29
|
+
*/
|
|
30
|
+
isConfigured(): boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export abstract class BaseChannelAdapter implements ChannelAdapter {
|
|
34
|
+
abstract readonly id: string;
|
|
35
|
+
abstract readonly type: string;
|
|
36
|
+
|
|
37
|
+
constructor(protected config: NotificationChannelConfig) {}
|
|
38
|
+
|
|
39
|
+
abstract initialize(): Promise<boolean>;
|
|
40
|
+
abstract send(payload: NotificationPayload): Promise<NotificationResult>;
|
|
41
|
+
|
|
42
|
+
isConfigured(): boolean {
|
|
43
|
+
return this.config.enabled;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Redact sensitive data from payload before sending
|
|
48
|
+
*/
|
|
49
|
+
protected redact(
|
|
50
|
+
payload: NotificationPayload,
|
|
51
|
+
patterns: RegExp[],
|
|
52
|
+
): NotificationPayload {
|
|
53
|
+
if (patterns.length === 0) return payload;
|
|
54
|
+
|
|
55
|
+
const redacted: string[] = [];
|
|
56
|
+
const details = payload.details ? { ...payload.details } : {};
|
|
57
|
+
|
|
58
|
+
for (const [key, value] of Object.entries(details)) {
|
|
59
|
+
const valStr = String(value);
|
|
60
|
+
for (const pattern of patterns) {
|
|
61
|
+
if (pattern.test(valStr)) {
|
|
62
|
+
redacted.push(key);
|
|
63
|
+
(details as Record<string, unknown>)[key] = "[REDACTED]";
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...payload,
|
|
71
|
+
details,
|
|
72
|
+
redacted: redacted.length > 0 ? redacted : undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Center — RFC-0022
|
|
3
|
+
*
|
|
4
|
+
* Mobile notification system for harness runtime events.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { NotificationCenter } from "./notification/index.js";
|
|
9
|
+
*
|
|
10
|
+
* const center = new NotificationCenter({
|
|
11
|
+
* channels: [
|
|
12
|
+
* {
|
|
13
|
+
* id: "my-telegram",
|
|
14
|
+
* type: "telegram",
|
|
15
|
+
* enabled: true,
|
|
16
|
+
* config: {
|
|
17
|
+
* botToken: process.env.TELEGRAM_BOT_TOKEN,
|
|
18
|
+
* chatId: "YOUR_CHAT_ID",
|
|
19
|
+
* },
|
|
20
|
+
* },
|
|
21
|
+
* ],
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* await center.initialize();
|
|
25
|
+
*
|
|
26
|
+
* await center.notify("JobStarted", {
|
|
27
|
+
* jobId: "job-123",
|
|
28
|
+
* requirement: "Build a REST API",
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export { NotificationCenter } from "./notification-center.js";
|
|
34
|
+
export { TelegramAdapter } from "./adapters/telegram-adapter.js";
|
|
35
|
+
export { NtfyAdapter } from "./adapters/ntfy-adapter.js";
|
|
36
|
+
export { EmailAdapter } from "./adapters/email-adapter.js";
|
|
37
|
+
export { WebhookAdapter } from "./adapters/webhook-adapter.js";
|
|
38
|
+
|
|
39
|
+
export type {
|
|
40
|
+
NotificationEvent,
|
|
41
|
+
NotificationPayload,
|
|
42
|
+
NotificationConfig,
|
|
43
|
+
NotificationChannelConfig,
|
|
44
|
+
NotificationChannelType,
|
|
45
|
+
NotificationResult,
|
|
46
|
+
NotificationContext,
|
|
47
|
+
TelegramConfig,
|
|
48
|
+
NtfyConfig,
|
|
49
|
+
EmailConfig,
|
|
50
|
+
WebhookConfig,
|
|
51
|
+
} from "./types.js";
|