pi-harness-runtime 0.3.2-beta.1 → 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.
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * run-minimax-auth.ts
4
+ *
5
+ * CLI to authenticate with MiniMax using Playwright persistent browser.
6
+ *
7
+ * Usage:
8
+ * bun packages/auth/src/run-minimax-auth.ts auth # First-time: login in browser
9
+ * bun packages/auth/src/run-minimax-auth.ts scrape # Scrape usage (silent)
10
+ * bun packages/auth/src/run-minimax-auth.ts check # Check status
11
+ * bun packages/auth/src/run-minimax-auth.ts open # Open browser with profile
12
+ *
13
+ * Security:
14
+ * - Human logs in manually in the browser
15
+ * - Playwright persistent profile auto-saves session
16
+ * - No credentials are processed by this code
17
+ */
18
+
19
+ import {
20
+ authenticateWithPersistentBrowser,
21
+ scrapeWithExistingProfile,
22
+ checkAuthStatus,
23
+ getProfileDir,
24
+ getStatusPath,
25
+ getLiveSessionPath,
26
+ startPersistentBrowserDaemon,
27
+ stopPersistentBrowserDaemon,
28
+ } from "./minimax-browser-auth.js";
29
+
30
+ async function main() {
31
+ const args = process.argv.slice(2);
32
+ const command = args[0] ?? "auth";
33
+
34
+ console.log("");
35
+ console.log("🔐 MiniMax Browser Authentication");
36
+ console.log("=".repeat(50));
37
+ console.log("");
38
+
39
+ if (command === "check") {
40
+ console.log("Command: check");
41
+ console.log("");
42
+ const status = await checkAuthStatus();
43
+ console.log("");
44
+ console.log("Status:");
45
+ console.log(JSON.stringify(status, null, 2));
46
+ console.log("");
47
+ console.log("Profile dir:", getProfileDir());
48
+ console.log("Status file:", getStatusPath());
49
+ return;
50
+ }
51
+
52
+ if (command === "scrape" || command === "usage") {
53
+ console.log(
54
+ "Command: scrape (prefers live browser, falls back to saved profile)",
55
+ );
56
+ console.log("");
57
+ const status = await scrapeWithExistingProfile();
58
+ console.log("");
59
+ console.log("Result:");
60
+ console.log(JSON.stringify(status, null, 2));
61
+ return;
62
+ }
63
+
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") {
106
+ console.log("Command: auth (persistent browser mode)");
107
+ console.log("");
108
+ console.log("This will:");
109
+ console.log("1. Launch a Chrome browser with persistent profile");
110
+ console.log("2. Navigate to MiniMax usage page");
111
+ console.log("3. Let you log in (first time only)");
112
+ console.log("4. Auto-save profile for future use");
113
+ console.log("");
114
+ console.log("Profile dir:", getProfileDir());
115
+ console.log("");
116
+
117
+ const status = await authenticateWithPersistentBrowser();
118
+ console.log("");
119
+ console.log("Result:");
120
+ console.log(JSON.stringify(status, null, 2));
121
+ return;
122
+ }
123
+
124
+ console.log("Usage:");
125
+ console.log(
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",
130
+ );
131
+ console.log(
132
+ " bun packages/auth/src/run-minimax-auth.ts scrape # Scrape usage (prefers live browser)",
133
+ );
134
+ console.log(
135
+ " bun packages/auth/src/run-minimax-auth.ts stop # Stop live browser daemon",
136
+ );
137
+ console.log(
138
+ " bun packages/auth/src/run-minimax-auth.ts check # Check status",
139
+ );
140
+ console.log("");
141
+ console.log("Security:");
142
+ console.log("- Human logs in manually in the browser");
143
+ console.log("- Playwright auto-saves persistent profile");
144
+ console.log("- No credentials are processed by this code");
145
+ }
146
+
147
+ main().catch((error) => {
148
+ console.error("Error:", error.message);
149
+ process.exit(1);
150
+ });
@@ -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";