arc402-cli 0.7.1 → 0.7.3
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/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +67 -0
- package/dist/commands/daemon.js.map +1 -1
- package/dist/daemon/config.d.ts +15 -1
- package/dist/daemon/config.d.ts.map +1 -1
- package/dist/daemon/config.js +32 -0
- package/dist/daemon/config.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +1 -9
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/notify.d.ts +35 -6
- package/dist/daemon/notify.d.ts.map +1 -1
- package/dist/daemon/notify.js +176 -48
- package/dist/daemon/notify.js.map +1 -1
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +5 -14
- package/dist/repl.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/daemon.ts +73 -0
- package/src/daemon/config.ts +35 -0
- package/src/daemon/index.ts +2 -14
- package/src/daemon/notify.ts +199 -59
- package/src/repl.ts +5 -18
package/src/commands/daemon.ts
CHANGED
|
@@ -19,7 +19,9 @@ import {
|
|
|
19
19
|
DAEMON_SOCK,
|
|
20
20
|
DAEMON_TOML,
|
|
21
21
|
TEMPLATE_DAEMON_TOML,
|
|
22
|
+
loadDaemonConfig,
|
|
22
23
|
} from "../daemon/config";
|
|
24
|
+
import { buildNotifier } from "../daemon/notify";
|
|
23
25
|
import {
|
|
24
26
|
buildOpenShellSecretExports,
|
|
25
27
|
buildOpenShellSshConfig,
|
|
@@ -943,6 +945,77 @@ export function registerDaemonCommands(program: Command): void {
|
|
|
943
945
|
console.log(" 5. Start the OpenShell-owned ARC-402 runtime: arc402 daemon start");
|
|
944
946
|
});
|
|
945
947
|
|
|
948
|
+
// ── daemon notifications ──────────────────────────────────────────────────────
|
|
949
|
+
const notifications = daemon
|
|
950
|
+
.command("notifications")
|
|
951
|
+
.description("Show or test configured notification channels");
|
|
952
|
+
|
|
953
|
+
notifications
|
|
954
|
+
.command("show")
|
|
955
|
+
.description("Show all configured notification channels")
|
|
956
|
+
.action(() => {
|
|
957
|
+
if (!fs.existsSync(DAEMON_TOML)) {
|
|
958
|
+
console.error("daemon.toml not found. Run `arc402 daemon init` first.");
|
|
959
|
+
process.exit(1);
|
|
960
|
+
}
|
|
961
|
+
const cfg = loadDaemonConfig();
|
|
962
|
+
const notif = cfg.notifications;
|
|
963
|
+
const channels: string[] = [];
|
|
964
|
+
|
|
965
|
+
if (notif.telegram_bot_token && notif.telegram_chat_id) {
|
|
966
|
+
channels.push(`telegram chat_id=${notif.telegram_chat_id}`);
|
|
967
|
+
}
|
|
968
|
+
if (notif.discord?.webhook_url) {
|
|
969
|
+
const u = new URL(notif.discord.webhook_url);
|
|
970
|
+
channels.push(`discord ${u.hostname}${u.pathname.slice(0, 30)}...`);
|
|
971
|
+
}
|
|
972
|
+
if (notif.webhook?.url) {
|
|
973
|
+
channels.push(`webhook ${notif.webhook.url}`);
|
|
974
|
+
}
|
|
975
|
+
if (notif.email?.smtp_host && notif.email?.to) {
|
|
976
|
+
channels.push(`email ${notif.email.smtp_host}:${notif.email.smtp_port} → ${notif.email.to}`);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (channels.length === 0) {
|
|
980
|
+
console.log("No notification channels configured.");
|
|
981
|
+
console.log("Edit ~/.arc402/daemon.toml to add Telegram, Discord, webhook, or email.");
|
|
982
|
+
} else {
|
|
983
|
+
console.log(`Configured channels (${channels.length}):`);
|
|
984
|
+
for (const ch of channels) console.log(` ${ch}`);
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
notifications
|
|
989
|
+
.command("test")
|
|
990
|
+
.description("Send a test message to all configured channels")
|
|
991
|
+
.action(async () => {
|
|
992
|
+
if (!fs.existsSync(DAEMON_TOML)) {
|
|
993
|
+
console.error("daemon.toml not found. Run `arc402 daemon init` first.");
|
|
994
|
+
process.exit(1);
|
|
995
|
+
}
|
|
996
|
+
const cfg = loadDaemonConfig();
|
|
997
|
+
const notifier = buildNotifier(cfg);
|
|
998
|
+
if (!notifier.isEnabled()) {
|
|
999
|
+
console.log("No notification channels configured. Nothing to test.");
|
|
1000
|
+
process.exit(0);
|
|
1001
|
+
}
|
|
1002
|
+
console.log("Sending test notification to all channels...");
|
|
1003
|
+
try {
|
|
1004
|
+
await notifier.send("daemon_started", "ARC-402 Test Notification",
|
|
1005
|
+
"This is a test message from arc402 daemon notifications test."
|
|
1006
|
+
);
|
|
1007
|
+
console.log("Test notification sent successfully.");
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
console.error(`Test notification failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1010
|
+
process.exit(1);
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// Default action: show (arc402 daemon notifications → arc402 daemon notifications show)
|
|
1015
|
+
notifications.action(() => {
|
|
1016
|
+
notifications.help();
|
|
1017
|
+
});
|
|
1018
|
+
|
|
946
1019
|
// ── daemon channel-watch ─────────────────────────────────────────────────────
|
|
947
1020
|
daemon
|
|
948
1021
|
.command("channel-watch")
|
package/src/daemon/config.ts
CHANGED
|
@@ -68,6 +68,9 @@ export interface DaemonConfig {
|
|
|
68
68
|
notify_on_channel_challenge: boolean;
|
|
69
69
|
notify_on_low_balance: boolean;
|
|
70
70
|
low_balance_threshold_eth: string;
|
|
71
|
+
discord: { webhook_url: string };
|
|
72
|
+
webhook: { url: string; headers: Record<string, string> };
|
|
73
|
+
email: { smtp_host: string; smtp_port: number; smtp_user: string; smtp_pass: string; to: string };
|
|
71
74
|
};
|
|
72
75
|
work: {
|
|
73
76
|
handler: "exec" | "http" | "noop";
|
|
@@ -114,6 +117,9 @@ function withDefaults(raw: Record<string, unknown>): DaemonConfig {
|
|
|
114
117
|
const wt = (raw.watchtower as Record<string, unknown>) ?? {};
|
|
115
118
|
const p = (raw.policy as Record<string, unknown>) ?? {};
|
|
116
119
|
const notif = (raw.notifications as Record<string, unknown>) ?? {};
|
|
120
|
+
const notifDiscord = (notif.discord as Record<string, unknown>) ?? {};
|
|
121
|
+
const notifWebhook = (notif.webhook as Record<string, unknown>) ?? {};
|
|
122
|
+
const notifEmail = (notif.email as Record<string, unknown>) ?? {};
|
|
117
123
|
const work = (raw.work as Record<string, unknown>) ?? {};
|
|
118
124
|
|
|
119
125
|
return {
|
|
@@ -169,6 +175,18 @@ function withDefaults(raw: Record<string, unknown>): DaemonConfig {
|
|
|
169
175
|
notify_on_channel_challenge: bool(notif.notify_on_channel_challenge, true),
|
|
170
176
|
notify_on_low_balance: bool(notif.notify_on_low_balance, true),
|
|
171
177
|
low_balance_threshold_eth: str(notif.low_balance_threshold_eth, "0.005"),
|
|
178
|
+
discord: { webhook_url: str(notifDiscord.webhook_url) },
|
|
179
|
+
webhook: {
|
|
180
|
+
url: str(notifWebhook.url),
|
|
181
|
+
headers: (notifWebhook.headers as Record<string, string>) ?? {},
|
|
182
|
+
},
|
|
183
|
+
email: {
|
|
184
|
+
smtp_host: str(notifEmail.smtp_host),
|
|
185
|
+
smtp_port: num(notifEmail.smtp_port, 587),
|
|
186
|
+
smtp_user: str(notifEmail.smtp_user),
|
|
187
|
+
smtp_pass: str(notifEmail.smtp_pass, "env:SMTP_PASS"),
|
|
188
|
+
to: str(notifEmail.to),
|
|
189
|
+
},
|
|
172
190
|
},
|
|
173
191
|
work: {
|
|
174
192
|
handler: (str(work.handler, "noop")) as "exec" | "http" | "noop",
|
|
@@ -213,6 +231,9 @@ export function loadDaemonConfig(configPath = DAEMON_TOML): DaemonConfig {
|
|
|
213
231
|
// Resolve optional env: values silently (missing = disabled feature)
|
|
214
232
|
config.notifications.telegram_bot_token = tryResolveEnvValue(config.notifications.telegram_bot_token);
|
|
215
233
|
config.notifications.telegram_chat_id = tryResolveEnvValue(config.notifications.telegram_chat_id);
|
|
234
|
+
config.notifications.discord.webhook_url = tryResolveEnvValue(config.notifications.discord.webhook_url);
|
|
235
|
+
config.notifications.webhook.url = tryResolveEnvValue(config.notifications.webhook.url);
|
|
236
|
+
config.notifications.email.smtp_pass = tryResolveEnvValue(config.notifications.email.smtp_pass);
|
|
216
237
|
config.work.http_auth_token = tryResolveEnvValue(config.work.http_auth_token);
|
|
217
238
|
|
|
218
239
|
return config;
|
|
@@ -300,6 +321,20 @@ notify_on_channel_challenge = true # Notify when watchtower submits a channel c
|
|
|
300
321
|
notify_on_low_balance = false # Disabled by default — enable if you want balance alerts
|
|
301
322
|
low_balance_threshold_eth = "0.005" # Balance alert threshold
|
|
302
323
|
|
|
324
|
+
[notifications.discord]
|
|
325
|
+
webhook_url = "" # Discord channel webhook URL (leave empty to disable)
|
|
326
|
+
|
|
327
|
+
[notifications.webhook]
|
|
328
|
+
url = "" # POST JSON {title, body, timestamp} to this URL (leave empty to disable)
|
|
329
|
+
# headers = { Authorization = "Bearer ..." } # Optional headers
|
|
330
|
+
|
|
331
|
+
[notifications.email]
|
|
332
|
+
smtp_host = "" # SMTP server hostname (leave empty to disable)
|
|
333
|
+
smtp_port = 587
|
|
334
|
+
smtp_user = "" # SMTP login / from address
|
|
335
|
+
smtp_pass = "env:SMTP_PASS" # Load from env, not hardcoded
|
|
336
|
+
to = "" # Recipient address
|
|
337
|
+
|
|
303
338
|
[work]
|
|
304
339
|
handler = "noop" # exec | http | noop
|
|
305
340
|
exec_command = "" # called with agreementId and spec as args (exec mode)
|
package/src/daemon/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
type DaemonConfig,
|
|
28
28
|
} from "./config";
|
|
29
29
|
import { verifyWallet, getWalletBalance } from "./wallet-monitor";
|
|
30
|
-
import {
|
|
30
|
+
import { buildNotifier } from "./notify";
|
|
31
31
|
import { HireListener } from "./hire-listener";
|
|
32
32
|
import { UserOpsManager, buildAcceptCalldata } from "./userops";
|
|
33
33
|
import { generateReceipt, extractLearnings, createJobDirectory, cleanJobDirectory } from "./job-lifecycle";
|
|
@@ -519,19 +519,7 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
// ── Setup notifier ───────────────────────────────────────────────────────
|
|
522
|
-
const notifier =
|
|
523
|
-
config.notifications.telegram_bot_token,
|
|
524
|
-
config.notifications.telegram_chat_id,
|
|
525
|
-
{
|
|
526
|
-
hire_request: config.notifications.notify_on_hire_request,
|
|
527
|
-
hire_accepted: config.notifications.notify_on_hire_accepted,
|
|
528
|
-
hire_rejected: config.notifications.notify_on_hire_rejected,
|
|
529
|
-
delivery: config.notifications.notify_on_delivery,
|
|
530
|
-
dispute: config.notifications.notify_on_dispute,
|
|
531
|
-
channel_challenge: config.notifications.notify_on_channel_challenge,
|
|
532
|
-
low_balance: config.notifications.notify_on_low_balance,
|
|
533
|
-
}
|
|
534
|
-
);
|
|
522
|
+
const notifier = buildNotifier(config);
|
|
535
523
|
|
|
536
524
|
// ── Step 10: Start relay listener ───────────────────────────────────────
|
|
537
525
|
const hireListener = new HireListener(config, db, notifier, config.wallet.contract_address);
|
package/src/daemon/notify.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Multi-channel notification module for daemon events.
|
|
3
|
+
* Supports Telegram, Discord webhooks, generic webhooks, and email (via nodemailer).
|
|
4
4
|
*/
|
|
5
5
|
import * as https from "https";
|
|
6
6
|
import * as http from "http";
|
|
7
|
+
import type { DaemonConfig } from "./config";
|
|
7
8
|
|
|
8
9
|
export type NotifyEvent =
|
|
9
10
|
| "hire_request"
|
|
@@ -16,33 +17,36 @@ export type NotifyEvent =
|
|
|
16
17
|
| "daemon_started"
|
|
17
18
|
| "daemon_stopped";
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// ─── Channel interface ────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface NotificationChannel {
|
|
23
|
+
send(title: string, body: string): Promise<void>;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
// ─── HTTP helper ──────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function httpPost(url: string, payload: string, extraHeaders: Record<string, string>): Promise<void> {
|
|
25
29
|
return new Promise((resolve, reject) => {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
const parsed = new URL(url);
|
|
31
|
+
const mod = parsed.protocol === "https:" ? https : http;
|
|
32
|
+
const options = {
|
|
33
|
+
hostname: parsed.hostname,
|
|
34
|
+
port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
|
|
35
|
+
path: parsed.pathname + parsed.search,
|
|
31
36
|
method: "POST",
|
|
32
37
|
headers: {
|
|
33
38
|
"Content-Type": "application/json",
|
|
34
39
|
"Content-Length": Buffer.byteLength(payload),
|
|
40
|
+
...extraHeaders,
|
|
35
41
|
},
|
|
36
42
|
};
|
|
37
|
-
const req =
|
|
38
|
-
|
|
39
|
-
res.on("data", (c: Buffer) => { data += c.toString(); });
|
|
43
|
+
const req = mod.request(options, (res) => {
|
|
44
|
+
res.resume();
|
|
40
45
|
res.on("end", () => {
|
|
41
|
-
|
|
42
|
-
if (!parsed.ok) {
|
|
43
|
-
reject(new Error(`Telegram API error: ${parsed.description}`));
|
|
44
|
-
} else {
|
|
46
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
45
47
|
resolve();
|
|
48
|
+
} else {
|
|
49
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
46
50
|
}
|
|
47
51
|
});
|
|
48
52
|
});
|
|
@@ -52,16 +56,118 @@ function telegramPost(botToken: string, method: string, body: object): Promise<v
|
|
|
52
56
|
});
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
// ─── Telegram channel ─────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export class TelegramChannel implements NotificationChannel {
|
|
62
|
+
constructor(private botToken: string, private chatId: string) {}
|
|
63
|
+
|
|
64
|
+
async send(title: string, body: string): Promise<void> {
|
|
65
|
+
const text = body ? `<b>${title}</b>\n${body}` : `<b>${title}</b>`;
|
|
66
|
+
const payload = JSON.stringify({ chat_id: this.chatId, text, parse_mode: "HTML" });
|
|
67
|
+
const options: https.RequestOptions = {
|
|
68
|
+
hostname: "api.telegram.org",
|
|
69
|
+
port: 443,
|
|
70
|
+
path: `/bot${this.botToken}/sendMessage`,
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: {
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
await new Promise<void>((resolve, reject) => {
|
|
78
|
+
const req = https.request(options, (res) => {
|
|
79
|
+
let data = "";
|
|
80
|
+
res.on("data", (c: Buffer) => { data += c.toString(); });
|
|
81
|
+
res.on("end", () => {
|
|
82
|
+
const parsed = JSON.parse(data) as { ok: boolean; description?: string };
|
|
83
|
+
if (!parsed.ok) {
|
|
84
|
+
reject(new Error(`Telegram API error: ${parsed.description}`));
|
|
85
|
+
} else {
|
|
86
|
+
resolve();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
req.on("error", reject);
|
|
91
|
+
req.write(payload);
|
|
92
|
+
req.end();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Discord channel ──────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export class DiscordChannel implements NotificationChannel {
|
|
100
|
+
constructor(private webhookUrl: string) {}
|
|
101
|
+
|
|
102
|
+
async send(title: string, body: string): Promise<void> {
|
|
103
|
+
const content = body ? `**${title}**\n${body}` : `**${title}**`;
|
|
104
|
+
await httpPost(this.webhookUrl, JSON.stringify({ content }), {});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Generic webhook channel ──────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export class WebhookChannel implements NotificationChannel {
|
|
111
|
+
constructor(private url: string, private headers: Record<string, string> = {}) {}
|
|
112
|
+
|
|
113
|
+
async send(title: string, body: string): Promise<void> {
|
|
114
|
+
await httpPost(
|
|
115
|
+
this.url,
|
|
116
|
+
JSON.stringify({ title, body, timestamp: new Date().toISOString() }),
|
|
117
|
+
this.headers
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Email channel (optional — requires nodemailer) ───────────────────────────
|
|
123
|
+
|
|
124
|
+
export class EmailChannel implements NotificationChannel {
|
|
125
|
+
constructor(private cfg: {
|
|
126
|
+
smtpHost: string;
|
|
127
|
+
smtpPort: number;
|
|
128
|
+
smtpUser: string;
|
|
129
|
+
smtpPass: string;
|
|
130
|
+
to: string;
|
|
131
|
+
}) {}
|
|
132
|
+
|
|
133
|
+
async send(title: string, body: string): Promise<void> {
|
|
134
|
+
// nodemailer is an optional runtime dependency — load via require to skip
|
|
135
|
+
// compile-time module resolution. Throws a clear message if not installed.
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any
|
|
137
|
+
let nodemailer: any;
|
|
138
|
+
try {
|
|
139
|
+
// Using Function constructor avoids TypeScript static import analysis.
|
|
140
|
+
nodemailer = (new Function("require", "return require('nodemailer')"))(require);
|
|
141
|
+
} catch {
|
|
142
|
+
throw new Error("nodemailer is not installed. Run: npm install nodemailer");
|
|
143
|
+
}
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
145
|
+
const transport = nodemailer.createTransport({
|
|
146
|
+
host: this.cfg.smtpHost,
|
|
147
|
+
port: this.cfg.smtpPort,
|
|
148
|
+
auth: { user: this.cfg.smtpUser, pass: this.cfg.smtpPass },
|
|
149
|
+
});
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
151
|
+
await transport.sendMail({
|
|
152
|
+
from: this.cfg.smtpUser,
|
|
153
|
+
to: this.cfg.to,
|
|
154
|
+
subject: title,
|
|
155
|
+
text: body,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Notifier ─────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
55
162
|
export class Notifier {
|
|
56
|
-
private
|
|
163
|
+
private channels: NotificationChannel[];
|
|
57
164
|
private notifyFlags: Record<NotifyEvent, boolean>;
|
|
58
165
|
|
|
59
166
|
constructor(
|
|
60
|
-
|
|
61
|
-
chatId: string,
|
|
167
|
+
channels: NotificationChannel[],
|
|
62
168
|
flags: Partial<Record<NotifyEvent, boolean>> = {}
|
|
63
169
|
) {
|
|
64
|
-
this.
|
|
170
|
+
this.channels = channels;
|
|
65
171
|
this.notifyFlags = {
|
|
66
172
|
hire_request: flags.hire_request ?? true,
|
|
67
173
|
hire_accepted: flags.hire_accepted ?? true,
|
|
@@ -76,82 +182,116 @@ export class Notifier {
|
|
|
76
182
|
}
|
|
77
183
|
|
|
78
184
|
isEnabled(): boolean {
|
|
79
|
-
return this.
|
|
185
|
+
return this.channels.length > 0;
|
|
80
186
|
}
|
|
81
187
|
|
|
82
|
-
async send(event: NotifyEvent,
|
|
83
|
-
if (!this.config) return;
|
|
188
|
+
async send(event: NotifyEvent, title: string, body: string): Promise<void> {
|
|
84
189
|
if (!this.notifyFlags[event]) return;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
190
|
+
await Promise.all(
|
|
191
|
+
this.channels.map(async (ch) => {
|
|
192
|
+
try {
|
|
193
|
+
await ch.send(title, body);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
process.stderr.write(`[notify] Channel send failed: ${err}\n`);
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
);
|
|
95
199
|
}
|
|
96
200
|
|
|
97
201
|
async notifyHireRequest(hireId: string, hirerAddress: string, priceEth: string, capability: string): Promise<void> {
|
|
98
202
|
const short = hirerAddress.slice(0, 10);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
`
|
|
102
|
-
`From: <code>${short}...</code>`,
|
|
203
|
+
await this.send("hire_request", "Hire Request", [
|
|
204
|
+
`ID: ${hireId}`,
|
|
205
|
+
`From: ${short}...`,
|
|
103
206
|
`Capability: ${capability || "unspecified"}`,
|
|
104
207
|
`Price: ${priceEth} ETH`,
|
|
105
208
|
``,
|
|
106
|
-
`Approve:
|
|
107
|
-
`Reject:
|
|
108
|
-
].join("\n");
|
|
109
|
-
await this.send("hire_request", msg);
|
|
209
|
+
`Approve: arc402 daemon approve ${hireId}`,
|
|
210
|
+
`Reject: arc402 daemon reject ${hireId}`,
|
|
211
|
+
].join("\n"));
|
|
110
212
|
}
|
|
111
213
|
|
|
112
214
|
async notifyHireAccepted(hireId: string, agreementId: string): Promise<void> {
|
|
113
|
-
await this.send("hire_accepted",
|
|
114
|
-
|
|
215
|
+
await this.send("hire_accepted", "Hire Accepted",
|
|
216
|
+
`ID: ${hireId}\nAgreement: ${agreementId}`
|
|
115
217
|
);
|
|
116
218
|
}
|
|
117
219
|
|
|
118
220
|
async notifyHireRejected(hireId: string, reason: string): Promise<void> {
|
|
119
|
-
await this.send("hire_rejected",
|
|
120
|
-
|
|
221
|
+
await this.send("hire_rejected", "Hire Rejected",
|
|
222
|
+
`ID: ${hireId}\nReason: ${reason}`
|
|
121
223
|
);
|
|
122
224
|
}
|
|
123
225
|
|
|
124
226
|
async notifyDelivery(agreementId: string, deliveryHash: string, userOpHash: string): Promise<void> {
|
|
125
|
-
await this.send("delivery",
|
|
126
|
-
|
|
127
|
-
|
|
227
|
+
await this.send("delivery", "Delivery Submitted", [
|
|
228
|
+
`Agreement: ${agreementId}`,
|
|
229
|
+
`Delivery hash: ${deliveryHash.slice(0, 16)}...`,
|
|
230
|
+
`UserOp: ${userOpHash.slice(0, 16)}...`,
|
|
231
|
+
].join("\n"));
|
|
128
232
|
}
|
|
129
233
|
|
|
130
234
|
async notifyDispute(agreementId: string, raisedBy: string): Promise<void> {
|
|
131
|
-
await this.send("dispute",
|
|
132
|
-
|
|
235
|
+
await this.send("dispute", "Dispute Raised",
|
|
236
|
+
`Agreement: ${agreementId}\nBy: ${raisedBy}`
|
|
133
237
|
);
|
|
134
238
|
}
|
|
135
239
|
|
|
136
240
|
async notifyChannelChallenge(channelId: string, txHash: string): Promise<void> {
|
|
137
|
-
await this.send("channel_challenge",
|
|
138
|
-
|
|
241
|
+
await this.send("channel_challenge", "Channel Challenged",
|
|
242
|
+
`Channel: ${channelId.slice(0, 16)}...\nTx: ${txHash.slice(0, 16)}...`
|
|
139
243
|
);
|
|
140
244
|
}
|
|
141
245
|
|
|
142
246
|
async notifyLowBalance(balanceEth: string, thresholdEth: string): Promise<void> {
|
|
143
|
-
await this.send("low_balance",
|
|
144
|
-
|
|
247
|
+
await this.send("low_balance", "Low Balance Alert",
|
|
248
|
+
`Current: ${balanceEth} ETH\nThreshold: ${thresholdEth} ETH`
|
|
145
249
|
);
|
|
146
250
|
}
|
|
147
251
|
|
|
148
252
|
async notifyStarted(walletAddress: string, subsystems: string[]): Promise<void> {
|
|
149
|
-
await this.send("daemon_started",
|
|
150
|
-
|
|
253
|
+
await this.send("daemon_started", "ARC-402 Daemon Started",
|
|
254
|
+
`Wallet: ${walletAddress}\nSubsystems: ${subsystems.join(", ")}`
|
|
151
255
|
);
|
|
152
256
|
}
|
|
153
257
|
|
|
154
258
|
async notifyStopped(): Promise<void> {
|
|
155
|
-
await this.send("daemon_stopped",
|
|
259
|
+
await this.send("daemon_stopped", "ARC-402 Daemon Stopped", "");
|
|
156
260
|
}
|
|
157
261
|
}
|
|
262
|
+
|
|
263
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
export function buildNotifier(config: DaemonConfig): Notifier {
|
|
266
|
+
const notif = config.notifications;
|
|
267
|
+
const channels: NotificationChannel[] = [];
|
|
268
|
+
|
|
269
|
+
if (notif.telegram_bot_token && notif.telegram_chat_id) {
|
|
270
|
+
channels.push(new TelegramChannel(notif.telegram_bot_token, notif.telegram_chat_id));
|
|
271
|
+
}
|
|
272
|
+
if (notif.discord?.webhook_url) {
|
|
273
|
+
channels.push(new DiscordChannel(notif.discord.webhook_url));
|
|
274
|
+
}
|
|
275
|
+
if (notif.webhook?.url) {
|
|
276
|
+
channels.push(new WebhookChannel(notif.webhook.url, notif.webhook.headers ?? {}));
|
|
277
|
+
}
|
|
278
|
+
if (notif.email?.smtp_host && notif.email?.smtp_user && notif.email?.to) {
|
|
279
|
+
channels.push(new EmailChannel({
|
|
280
|
+
smtpHost: notif.email.smtp_host,
|
|
281
|
+
smtpPort: notif.email.smtp_port,
|
|
282
|
+
smtpUser: notif.email.smtp_user,
|
|
283
|
+
smtpPass: notif.email.smtp_pass,
|
|
284
|
+
to: notif.email.to,
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return new Notifier(channels, {
|
|
289
|
+
hire_request: notif.notify_on_hire_request,
|
|
290
|
+
hire_accepted: notif.notify_on_hire_accepted,
|
|
291
|
+
hire_rejected: notif.notify_on_hire_rejected,
|
|
292
|
+
delivery: notif.notify_on_delivery,
|
|
293
|
+
dispute: notif.notify_on_dispute,
|
|
294
|
+
channel_challenge: notif.notify_on_channel_challenge,
|
|
295
|
+
low_balance: notif.notify_on_low_balance,
|
|
296
|
+
});
|
|
297
|
+
}
|
package/src/repl.ts
CHANGED
|
@@ -231,25 +231,11 @@ class TUI {
|
|
|
231
231
|
private setupScreen(): void {
|
|
232
232
|
this.bannerLines = getBannerLines(this.bannerCfg);
|
|
233
233
|
|
|
234
|
-
|
|
235
|
-
write(ansi.clearScreen + ansi.home);
|
|
236
|
-
|
|
237
|
-
// Banner
|
|
234
|
+
// Simple setup — print banner once, no scroll regions (breaks on macOS Terminal)
|
|
238
235
|
for (const line of this.bannerLines) {
|
|
239
236
|
write(line + "\n");
|
|
240
237
|
}
|
|
241
|
-
|
|
242
|
-
// Separator between banner and output area
|
|
243
|
-
write(chalk.dim("─".repeat(this.termCols)) + "\n");
|
|
244
|
-
|
|
245
|
-
// Set scroll region (output area, leaves last row free for input)
|
|
246
|
-
if (this.scrollTop <= this.scrollBot) {
|
|
247
|
-
write(ansi.scrollRegion(this.scrollTop, this.scrollBot));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Position cursor at top of output area
|
|
251
|
-
write(ansi.move(this.scrollTop, 1));
|
|
252
|
-
write(ansi.showCursor);
|
|
238
|
+
write(chalk.dim("─".repeat(Math.min(this.termCols, 60))) + "\n\n");
|
|
253
239
|
}
|
|
254
240
|
|
|
255
241
|
// ── Banner repaint (in-place, preserves output area) ────────────────────────
|
|
@@ -664,10 +650,11 @@ class TUI {
|
|
|
664
650
|
if (buffer.trim()) flushLine(buffer);
|
|
665
651
|
}
|
|
666
652
|
|
|
667
|
-
// ── After each command:
|
|
653
|
+
// ── After each command: show prompt ──────────────────────────────────────────
|
|
668
654
|
|
|
669
655
|
private afterCommand(): void {
|
|
670
|
-
|
|
656
|
+
// Simple prompt — no banner repaint (causes scrollback issues on macOS Terminal)
|
|
657
|
+
write("\n");
|
|
671
658
|
this.drawInputLine();
|
|
672
659
|
}
|
|
673
660
|
|