pilotlynx 0.1.2 → 0.2.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/README.md +40 -40
- package/dist/agents/improve.agent.d.ts +42 -1
- package/dist/agents/improve.agent.js +102 -7
- package/dist/agents/improve.agent.js.map +1 -1
- package/dist/agents/run.agent.d.ts +0 -1
- package/dist/agents/run.agent.js +10 -7
- package/dist/agents/run.agent.js.map +1 -1
- package/dist/cli.js +8 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.js +51 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/eval.d.ts +2 -0
- package/dist/commands/eval.js +47 -0
- package/dist/commands/eval.js.map +1 -0
- package/dist/commands/improve.js +83 -5
- package/dist/commands/improve.js.map +1 -1
- package/dist/commands/init.js +17 -27
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/insights.js +63 -0
- package/dist/commands/insights.js.map +1 -1
- package/dist/commands/logs.d.ts +1 -0
- package/dist/commands/logs.js +23 -1
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/relay.js +250 -124
- package/dist/commands/relay.js.map +1 -1
- package/dist/commands/run.js +21 -2
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/schedule.js +7 -1
- package/dist/commands/schedule.js.map +1 -1
- package/dist/commands/status.js +9 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/lib/agent-runner.d.ts +5 -1
- package/dist/lib/agent-runner.js +41 -1
- package/dist/lib/agent-runner.js.map +1 -1
- package/dist/lib/audit.d.ts +7 -0
- package/dist/lib/audit.js +63 -0
- package/dist/lib/audit.js.map +1 -0
- package/dist/lib/callbacks.d.ts +12 -1
- package/dist/lib/callbacks.js +147 -19
- package/dist/lib/callbacks.js.map +1 -1
- package/dist/lib/command-ops/doctor-ops.js +41 -6
- package/dist/lib/command-ops/doctor-ops.js.map +1 -1
- package/dist/lib/command-ops/eval-ops.d.ts +7 -0
- package/dist/lib/command-ops/eval-ops.js +111 -0
- package/dist/lib/command-ops/eval-ops.js.map +1 -0
- package/dist/lib/command-ops/improve-ops.d.ts +14 -1
- package/dist/lib/command-ops/improve-ops.js +369 -17
- package/dist/lib/command-ops/improve-ops.js.map +1 -1
- package/dist/lib/command-ops/run-ops.d.ts +8 -1
- package/dist/lib/command-ops/run-ops.js +25 -4
- package/dist/lib/command-ops/run-ops.js.map +1 -1
- package/dist/lib/command-ops/secrets-migration-ops.js +1 -1
- package/dist/lib/command-ops/secrets-migration-ops.js.map +1 -1
- package/dist/lib/command-ops/status-ops.d.ts +1 -0
- package/dist/lib/command-ops/status-ops.js +19 -7
- package/dist/lib/command-ops/status-ops.js.map +1 -1
- package/dist/lib/config.js +3 -3
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/cron.d.ts +1 -1
- package/dist/lib/cron.js +2 -2
- package/dist/lib/cron.js.map +1 -1
- package/dist/lib/global-config.js +1 -1
- package/dist/lib/global-config.js.map +1 -1
- package/dist/lib/observation.d.ts +60 -2
- package/dist/lib/observation.js +261 -13
- package/dist/lib/observation.js.map +1 -1
- package/dist/lib/registry.d.ts +0 -1
- package/dist/lib/registry.js +1 -1
- package/dist/lib/registry.js.map +1 -1
- package/dist/lib/relay/admin.d.ts +29 -0
- package/dist/lib/relay/admin.js +176 -0
- package/dist/lib/relay/admin.js.map +1 -0
- package/dist/lib/relay/bindings.d.ts +8 -0
- package/dist/lib/relay/bindings.js +50 -0
- package/dist/lib/relay/bindings.js.map +1 -0
- package/dist/lib/relay/config.d.ts +3 -3
- package/dist/lib/relay/config.js +18 -10
- package/dist/lib/relay/config.js.map +1 -1
- package/dist/lib/relay/context.d.ts +31 -0
- package/dist/lib/relay/context.js +118 -0
- package/dist/lib/relay/context.js.map +1 -0
- package/dist/lib/relay/db.d.ts +38 -0
- package/dist/lib/relay/db.js +252 -0
- package/dist/lib/relay/db.js.map +1 -0
- package/dist/lib/relay/executor.d.ts +2 -0
- package/dist/lib/relay/executor.js +108 -0
- package/dist/lib/relay/executor.js.map +1 -0
- package/dist/lib/relay/feedback.d.ts +29 -0
- package/dist/lib/relay/feedback.js +142 -0
- package/dist/lib/relay/feedback.js.map +1 -0
- package/dist/lib/relay/notifier.d.ts +30 -0
- package/dist/lib/relay/notifier.js +78 -0
- package/dist/lib/relay/notifier.js.map +1 -0
- package/dist/lib/relay/notify.d.ts +3 -4
- package/dist/lib/relay/notify.js +43 -159
- package/dist/lib/relay/notify.js.map +1 -1
- package/dist/lib/relay/platform.d.ts +52 -0
- package/dist/lib/relay/platform.js +5 -0
- package/dist/lib/relay/platform.js.map +1 -0
- package/dist/lib/relay/platforms/slack.d.ts +37 -0
- package/dist/lib/relay/platforms/slack.js +240 -0
- package/dist/lib/relay/platforms/slack.js.map +1 -0
- package/dist/lib/relay/platforms/telegram.d.ts +29 -0
- package/dist/lib/relay/platforms/telegram.js +193 -0
- package/dist/lib/relay/platforms/telegram.js.map +1 -0
- package/dist/lib/relay/poster.d.ts +24 -0
- package/dist/lib/relay/poster.js +136 -0
- package/dist/lib/relay/poster.js.map +1 -0
- package/dist/lib/relay/queue.d.ts +18 -0
- package/dist/lib/relay/queue.js +85 -0
- package/dist/lib/relay/queue.js.map +1 -0
- package/dist/lib/relay/router.d.ts +25 -2
- package/dist/lib/relay/router.js +259 -168
- package/dist/lib/relay/router.js.map +1 -1
- package/dist/lib/relay/service.d.ts +31 -7
- package/dist/lib/relay/service.js +281 -200
- package/dist/lib/relay/service.js.map +1 -1
- package/dist/lib/relay/types.d.ts +189 -34
- package/dist/lib/relay/types.js +68 -28
- package/dist/lib/relay/types.js.map +1 -1
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +17 -2
- package/dist/lib/sandbox.js.map +1 -1
- package/dist/lib/schedule.d.ts +4 -5
- package/dist/lib/schedule.js +7 -8
- package/dist/lib/schedule.js.map +1 -1
- package/dist/lib/secrets.js +11 -1
- package/dist/lib/secrets.js.map +1 -1
- package/dist/lib/types.d.ts +80 -0
- package/dist/lib/types.js +9 -1
- package/dist/lib/types.js.map +1 -1
- package/package.json +18 -18
- package/prompts/improve.yaml +114 -6
- package/prompts/relay.yaml +29 -0
- package/prompts/run.yaml +36 -2
- package/template/CLAUDE.md +34 -5
- package/template/RUNBOOK.md +5 -5
- package/template/evals/sample.json +16 -0
- package/template/memory/MEMORY.md +6 -0
- package/template/memory/procedures/.gitkeep +0 -0
- package/template/schedule.yaml +1 -1
- package/template/workflows/daily_feedback.ts +78 -2
- package/template/workflows/project_review.ts +51 -2
- package/prompts/relay-chat.yaml +0 -24
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// ── Feedback Handler ──
|
|
2
|
+
// Classifies chat reactions as feedback signals and persists them.
|
|
3
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { INSIGHTS_DIR, getProjectDir } from '../config.js';
|
|
6
|
+
const FEEDBACK_FILE = () => join(INSIGHTS_DIR(), 'relay-feedback.jsonl');
|
|
7
|
+
const MAX_ENTRIES = 1000;
|
|
8
|
+
// ── Emoji Classification ──
|
|
9
|
+
const POSITIVE_EMOJI = new Set(['thumbsup', '+1', 'white_check_mark', 'heavy_check_mark']);
|
|
10
|
+
const NEGATIVE_EMOJI = new Set(['thumbsdown', '-1', 'x', 'negative_squared_cross_mark']);
|
|
11
|
+
const SAVE_EMOJI = new Set(['star', 'star2', 'glowing_star']);
|
|
12
|
+
const ACKNOWLEDGE_EMOJI = new Set(['eyes', 'mag']);
|
|
13
|
+
/**
|
|
14
|
+
* Classify a reaction emoji into a feedback type.
|
|
15
|
+
* Returns null for unrecognized emoji.
|
|
16
|
+
*/
|
|
17
|
+
export function classifyReaction(emoji) {
|
|
18
|
+
const key = emoji.replace(/:/g, '').toLowerCase();
|
|
19
|
+
if (POSITIVE_EMOJI.has(key))
|
|
20
|
+
return 'positive';
|
|
21
|
+
if (NEGATIVE_EMOJI.has(key))
|
|
22
|
+
return 'negative';
|
|
23
|
+
if (SAVE_EMOJI.has(key))
|
|
24
|
+
return 'save';
|
|
25
|
+
if (ACKNOWLEDGE_EMOJI.has(key))
|
|
26
|
+
return 'acknowledge';
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Convert a FeedbackSignal into a ChatFeedbackEntry.
|
|
31
|
+
*/
|
|
32
|
+
export function handleFeedback(signal, project, agentOutputSummary) {
|
|
33
|
+
return {
|
|
34
|
+
type: signal.type,
|
|
35
|
+
platform: signal.platform,
|
|
36
|
+
conversationId: signal.conversationId,
|
|
37
|
+
messageId: signal.messageId,
|
|
38
|
+
userId: signal.userId,
|
|
39
|
+
userName: signal.userName,
|
|
40
|
+
timestamp: signal.timestamp,
|
|
41
|
+
project,
|
|
42
|
+
agentOutputSummary,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Append a feedback entry to the relay feedback JSONL file.
|
|
47
|
+
* Caps at MAX_ENTRIES by trimming oldest entries when exceeded.
|
|
48
|
+
*/
|
|
49
|
+
export function appendRelayFeedback(entry) {
|
|
50
|
+
const filePath = FEEDBACK_FILE();
|
|
51
|
+
const dir = dirname(filePath);
|
|
52
|
+
if (!existsSync(dir))
|
|
53
|
+
mkdirSync(dir, { recursive: true });
|
|
54
|
+
appendFileSync(filePath, JSON.stringify(entry) + '\n', 'utf8');
|
|
55
|
+
// Check if we need to trim
|
|
56
|
+
const lines = readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
|
|
57
|
+
if (lines.length > MAX_ENTRIES) {
|
|
58
|
+
const trimmed = lines.slice(lines.length - MAX_ENTRIES);
|
|
59
|
+
writeFileSync(filePath, trimmed.join('\n') + '\n', 'utf8');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read all feedback entries, optionally filtered by project.
|
|
64
|
+
*/
|
|
65
|
+
export function readRelayFeedback(project) {
|
|
66
|
+
const filePath = FEEDBACK_FILE();
|
|
67
|
+
if (!existsSync(filePath))
|
|
68
|
+
return [];
|
|
69
|
+
const lines = readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
|
|
70
|
+
const entries = [];
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
try {
|
|
73
|
+
const entry = JSON.parse(line);
|
|
74
|
+
if (!project || entry.project === project) {
|
|
75
|
+
entries.push(entry);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// skip malformed lines
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return entries;
|
|
83
|
+
}
|
|
84
|
+
// ── Save to Memory ──
|
|
85
|
+
/**
|
|
86
|
+
* Save a starred/saved response to the project's memory/ directory.
|
|
87
|
+
* Creates a timestamped markdown file with the thread context and bot response.
|
|
88
|
+
*/
|
|
89
|
+
export function saveFeedbackToMemory(project, entry, botResponse) {
|
|
90
|
+
let projectDir;
|
|
91
|
+
try {
|
|
92
|
+
projectDir = getProjectDir(project);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return; // project not found — skip silently
|
|
96
|
+
}
|
|
97
|
+
const memoryDir = join(projectDir, 'memory');
|
|
98
|
+
if (!existsSync(memoryDir))
|
|
99
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
100
|
+
const ts = entry.timestamp.replace(/[:.]/g, '-').slice(0, 19);
|
|
101
|
+
const filename = `relay-saved-${ts}.md`;
|
|
102
|
+
const filePath = join(memoryDir, filename);
|
|
103
|
+
const lines = [
|
|
104
|
+
`# Saved Relay Response`,
|
|
105
|
+
``,
|
|
106
|
+
`- **Platform**: ${entry.platform}`,
|
|
107
|
+
`- **User**: ${entry.userName}`,
|
|
108
|
+
`- **Date**: ${entry.timestamp}`,
|
|
109
|
+
`- **Conversation**: ${entry.conversationId}`,
|
|
110
|
+
``,
|
|
111
|
+
];
|
|
112
|
+
if (botResponse) {
|
|
113
|
+
lines.push(`## Response`, ``, botResponse, ``);
|
|
114
|
+
}
|
|
115
|
+
if (entry.agentOutputSummary) {
|
|
116
|
+
lines.push(`## Agent Context`, ``, entry.agentOutputSummary, ``);
|
|
117
|
+
}
|
|
118
|
+
writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
119
|
+
}
|
|
120
|
+
// ── Rate Limiting ──
|
|
121
|
+
const reactionTimestamps = new Map();
|
|
122
|
+
/**
|
|
123
|
+
* Check if a user has exceeded the reaction rate limit (sliding window, 1 hour).
|
|
124
|
+
*/
|
|
125
|
+
export function isReactionRateLimited(userId, maxPerHour) {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const windowMs = 60 * 60 * 1000;
|
|
128
|
+
const cutoff = now - windowMs;
|
|
129
|
+
let timestamps = reactionTimestamps.get(userId);
|
|
130
|
+
if (!timestamps) {
|
|
131
|
+
timestamps = [];
|
|
132
|
+
reactionTimestamps.set(userId, timestamps);
|
|
133
|
+
}
|
|
134
|
+
// Prune old entries
|
|
135
|
+
const recent = timestamps.filter((t) => t > cutoff);
|
|
136
|
+
reactionTimestamps.set(userId, recent);
|
|
137
|
+
if (recent.length >= maxPerHour)
|
|
138
|
+
return true;
|
|
139
|
+
recent.push(now);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=feedback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feedback.js","sourceRoot":"","sources":["../../../src/lib/relay/feedback.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { ChatPlatform } from './platform.js';
|
|
3
|
+
import type { RunRecord } from '../types.js';
|
|
4
|
+
import type { RelayConfig } from './types.js';
|
|
5
|
+
export declare class RelayNotifier {
|
|
6
|
+
private platforms;
|
|
7
|
+
private db;
|
|
8
|
+
private config;
|
|
9
|
+
constructor(platforms: Map<string, ChatPlatform>, db: Database.Database, config: RelayConfig);
|
|
10
|
+
/**
|
|
11
|
+
* Notify bound channels about a scheduled workflow run result.
|
|
12
|
+
*/
|
|
13
|
+
notifyScheduleResult(project: string, record: RunRecord): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Notify bound channels about insights from the improve loop.
|
|
16
|
+
*/
|
|
17
|
+
notifyImproveInsights(project: string, insights: string[]): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Notify bound channels about budget threshold being approached.
|
|
20
|
+
*/
|
|
21
|
+
notifyBudgetAlert(project: string, spent: number, limit: number): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Notify bound channels when a project's health score drops.
|
|
24
|
+
*/
|
|
25
|
+
notifyHealthDrop(project: string, oldScore: number, newScore: number): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Send a message to all platforms that have a channel bound to the project.
|
|
28
|
+
*/
|
|
29
|
+
private broadcast;
|
|
30
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ── Relay Notifier ──
|
|
2
|
+
// Sends proactive notifications to bound channels for scheduled runs,
|
|
3
|
+
// improve insights, budget alerts, and health score changes.
|
|
4
|
+
import { getChannelForProject } from './bindings.js';
|
|
5
|
+
export class RelayNotifier {
|
|
6
|
+
platforms;
|
|
7
|
+
db;
|
|
8
|
+
config;
|
|
9
|
+
constructor(platforms, db, config) {
|
|
10
|
+
this.platforms = platforms;
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.config = config;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Notify bound channels about a scheduled workflow run result.
|
|
16
|
+
*/
|
|
17
|
+
async notifyScheduleResult(project, record) {
|
|
18
|
+
if (!this.config.notifications.scheduleFailures && !record.success)
|
|
19
|
+
return;
|
|
20
|
+
const start = new Date(record.startedAt).getTime();
|
|
21
|
+
const end = new Date(record.completedAt).getTime();
|
|
22
|
+
const dur = Math.round((end - start) / 1000);
|
|
23
|
+
const status = record.success ? 'Success' : 'Failed';
|
|
24
|
+
let message = `Scheduled run \`${record.workflow}\` for *${project}*: ${status}\nCost: $${record.costUsd.toFixed(4)} | Duration: ${dur}s`;
|
|
25
|
+
if (!record.success && record.error) {
|
|
26
|
+
message += `\n${record.error}`;
|
|
27
|
+
}
|
|
28
|
+
await this.broadcast(project, message);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Notify bound channels about insights from the improve loop.
|
|
32
|
+
*/
|
|
33
|
+
async notifyImproveInsights(project, insights) {
|
|
34
|
+
if (!this.config.notifications.improveInsights)
|
|
35
|
+
return;
|
|
36
|
+
if (insights.length === 0)
|
|
37
|
+
return;
|
|
38
|
+
const bullets = insights.map((i) => ` - ${i}`).join('\n');
|
|
39
|
+
const message = `Improve loop found insights for *${project}*:\n${bullets}`;
|
|
40
|
+
await this.broadcast(project, message);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Notify bound channels about budget threshold being approached.
|
|
44
|
+
*/
|
|
45
|
+
async notifyBudgetAlert(project, spent, limit) {
|
|
46
|
+
if (!this.config.notifications.budgetAlerts)
|
|
47
|
+
return;
|
|
48
|
+
const pct = Math.round((spent / limit) * 100);
|
|
49
|
+
const message = `Budget alert for *${project}*: $${spent.toFixed(2)} of $${limit.toFixed(2)} daily budget used (${pct}%)`;
|
|
50
|
+
await this.broadcast(project, message);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Notify bound channels when a project's health score drops.
|
|
54
|
+
*/
|
|
55
|
+
async notifyHealthDrop(project, oldScore, newScore) {
|
|
56
|
+
if (newScore >= this.config.notifications.healthScoreThreshold)
|
|
57
|
+
return;
|
|
58
|
+
const message = `Health score dropped for *${project}*: ${oldScore} -> ${newScore}`;
|
|
59
|
+
await this.broadcast(project, message);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Send a message to all platforms that have a channel bound to the project.
|
|
63
|
+
*/
|
|
64
|
+
async broadcast(project, message) {
|
|
65
|
+
for (const [platformName, platform] of this.platforms) {
|
|
66
|
+
const channelId = getChannelForProject(this.db, platformName, project);
|
|
67
|
+
if (!channelId)
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
await platform.sendMessage(channelId, message);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error(`[relay-notifier] Failed to send to ${platformName}/${channelId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=notifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifier.js","sourceRoot":"","sources":["../../../src/lib/relay/notifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
import type { WebhookPayload } from './types.js';
|
|
1
2
|
import type { RunRecord } from '../types.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Uses stateless Bot.api (no running process needed).
|
|
5
|
-
*/
|
|
3
|
+
export declare function sendWebhookNotification(payload: WebhookPayload): Promise<void>;
|
|
4
|
+
/** Converts a RunRecord into a WebhookPayload and sends it to configured webhooks. */
|
|
6
5
|
export declare function sendRunNotification(record: RunRecord): Promise<void>;
|
package/dist/lib/relay/notify.js
CHANGED
|
@@ -1,168 +1,52 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (host === '0.0.0.0')
|
|
23
|
-
return false;
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function formatRunMessage(record) {
|
|
31
|
-
const icon = record.success ? '\u2705' : '\u274c';
|
|
32
|
-
const status = record.success ? 'Success' : 'Failed';
|
|
33
|
-
const duration = (() => {
|
|
34
|
-
const start = new Date(record.startedAt).getTime();
|
|
35
|
-
const end = new Date(record.completedAt).getTime();
|
|
36
|
-
const secs = Math.round((end - start) / 1000);
|
|
37
|
-
return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s`;
|
|
38
|
-
})();
|
|
39
|
-
let msg = `${icon} *${record.project}/${record.workflow}* — ${status}\n`;
|
|
40
|
-
msg += `Duration: ${duration} | Cost: $${record.costUsd.toFixed(4)} | Turns: ${record.numTurns}\n`;
|
|
41
|
-
if (record.error) {
|
|
42
|
-
const truncated = record.error.length > 200 ? record.error.slice(0, 200) + '...' : record.error;
|
|
43
|
-
msg += `Error: ${truncated}`;
|
|
44
|
-
}
|
|
45
|
-
else if (record.summary) {
|
|
46
|
-
const truncated = record.summary.length > 300 ? record.summary.slice(0, 300) + '...' : record.summary;
|
|
47
|
-
msg += truncated;
|
|
48
|
-
}
|
|
49
|
-
return msg;
|
|
50
|
-
}
|
|
51
|
-
const MAX_DEAD_LETTERS = 1000;
|
|
52
|
-
async function logDeadLetter(entry) {
|
|
53
|
-
const dir = getRelayDir();
|
|
54
|
-
mkdirSync(dir, { recursive: true });
|
|
55
|
-
const filePath = `${dir}/dead-letters.jsonl`;
|
|
56
|
-
if (!existsSync(filePath)) {
|
|
57
|
-
writeFileSync(filePath, '', 'utf8');
|
|
58
|
-
}
|
|
59
|
-
let release;
|
|
60
|
-
try {
|
|
61
|
-
release = await lock(filePath, { stale: 10_000, retries: { retries: 3, minTimeout: 100 } });
|
|
62
|
-
const content = readFileSync(filePath, 'utf8').trim();
|
|
63
|
-
const lines = content ? content.split('\n') : [];
|
|
64
|
-
lines.push(JSON.stringify(entry));
|
|
65
|
-
// Trim to last MAX_DEAD_LETTERS entries
|
|
66
|
-
const trimmed = lines.length > MAX_DEAD_LETTERS ? lines.slice(-MAX_DEAD_LETTERS) : lines;
|
|
67
|
-
writeFileSync(filePath, trimmed.join('\n') + '\n', 'utf8');
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
// Best-effort: fall back to simple append if locking fails
|
|
71
|
-
appendFileSync(filePath, JSON.stringify(entry) + '\n', 'utf8');
|
|
72
|
-
}
|
|
73
|
-
finally {
|
|
74
|
-
if (release)
|
|
75
|
-
await release();
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
async function sendWithRetry(fn, chatId, channel, payload, retries = 3) {
|
|
79
|
-
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
1
|
+
import { loadWebhookConfig } from './config.js';
|
|
2
|
+
export async function sendWebhookNotification(payload) {
|
|
3
|
+
const config = loadWebhookConfig();
|
|
4
|
+
if (!config?.enabled)
|
|
5
|
+
return;
|
|
6
|
+
for (const webhook of config.webhooks) {
|
|
7
|
+
if (!webhook.events.includes(payload.event))
|
|
8
|
+
continue;
|
|
9
|
+
const headers = {
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
'User-Agent': 'PilotLynx-Webhook/1.0',
|
|
12
|
+
...webhook.headers,
|
|
13
|
+
};
|
|
14
|
+
if (webhook.secret) {
|
|
15
|
+
const crypto = await import('node:crypto');
|
|
16
|
+
const body = JSON.stringify(payload);
|
|
17
|
+
const signature = crypto.createHmac('sha256', webhook.secret)
|
|
18
|
+
.update(body)
|
|
19
|
+
.digest('hex');
|
|
20
|
+
headers['X-PilotLynx-Signature'] = `sha256=${signature}`;
|
|
21
|
+
}
|
|
80
22
|
try {
|
|
81
|
-
await
|
|
82
|
-
|
|
23
|
+
await fetch(webhook.url, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers,
|
|
26
|
+
body: JSON.stringify(payload),
|
|
27
|
+
signal: AbortSignal.timeout(10_000),
|
|
28
|
+
});
|
|
83
29
|
}
|
|
84
30
|
catch (err) {
|
|
85
|
-
|
|
86
|
-
// Don't retry 4xx errors (except 429 rate limits)
|
|
87
|
-
if (status && status >= 400 && status < 500 && status !== 429) {
|
|
88
|
-
await logDeadLetter({
|
|
89
|
-
timestamp: new Date().toISOString(),
|
|
90
|
-
chatId,
|
|
91
|
-
channel,
|
|
92
|
-
error: err instanceof Error ? err.message : String(err),
|
|
93
|
-
payload,
|
|
94
|
-
});
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
if (attempt === retries) {
|
|
98
|
-
await logDeadLetter({
|
|
99
|
-
timestamp: new Date().toISOString(),
|
|
100
|
-
chatId,
|
|
101
|
-
channel,
|
|
102
|
-
error: err instanceof Error ? err.message : String(err),
|
|
103
|
-
payload,
|
|
104
|
-
});
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
// Exponential backoff: 1s, 2s, 4s
|
|
108
|
-
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));
|
|
31
|
+
console.error(`[pilotlynx] Webhook "${webhook.name}" failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
109
32
|
}
|
|
110
33
|
}
|
|
111
34
|
}
|
|
112
|
-
/**
|
|
113
|
-
* Send a notification for a completed workflow run to all configured chats.
|
|
114
|
-
* Uses stateless Bot.api (no running process needed).
|
|
115
|
-
*/
|
|
35
|
+
/** Converts a RunRecord into a WebhookPayload and sends it to configured webhooks. */
|
|
116
36
|
export async function sendRunNotification(record) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (chatConfig.project && chatConfig.project !== record.project)
|
|
132
|
-
continue;
|
|
133
|
-
if (chatId.startsWith('telegram:')) {
|
|
134
|
-
if (!config.channels.telegram.enabled)
|
|
135
|
-
continue;
|
|
136
|
-
const token = getTelegramToken();
|
|
137
|
-
if (!token)
|
|
138
|
-
continue;
|
|
139
|
-
const tgChatId = chatId.slice('telegram:'.length);
|
|
140
|
-
const api = new Api(token);
|
|
141
|
-
await sendWithRetry(() => api.sendMessage(tgChatId, message, { parse_mode: 'Markdown' }).then(() => { }), chatId, 'telegram', record);
|
|
142
|
-
}
|
|
143
|
-
else if (chatId.startsWith('webhook:')) {
|
|
144
|
-
if (!config.channels.webhook.enabled)
|
|
145
|
-
continue;
|
|
146
|
-
const url = chatId.slice('webhook:'.length);
|
|
147
|
-
if (!isWebhookUrlSafe(url)) {
|
|
148
|
-
await logDeadLetter({
|
|
149
|
-
timestamp: new Date().toISOString(),
|
|
150
|
-
chatId,
|
|
151
|
-
channel: 'webhook',
|
|
152
|
-
error: `Unsafe webhook URL rejected: ${url}`,
|
|
153
|
-
payload: record,
|
|
154
|
-
});
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
await sendWithRetry(() => fetch(url, {
|
|
158
|
-
method: 'POST',
|
|
159
|
-
headers: { 'Content-Type': 'application/json' },
|
|
160
|
-
body: JSON.stringify(record),
|
|
161
|
-
}).then(res => {
|
|
162
|
-
if (!res.ok)
|
|
163
|
-
throw Object.assign(new Error(`HTTP ${res.status}`), { status: res.status });
|
|
164
|
-
}), chatId, 'webhook', record);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
37
|
+
const start = new Date(record.startedAt).getTime();
|
|
38
|
+
const end = new Date(record.completedAt).getTime();
|
|
39
|
+
const payload = {
|
|
40
|
+
event: record.success ? 'run_complete' : 'run_failed',
|
|
41
|
+
timestamp: record.completedAt,
|
|
42
|
+
project: record.project,
|
|
43
|
+
workflow: record.workflow,
|
|
44
|
+
success: record.success,
|
|
45
|
+
summary: record.error ?? record.summary ?? '',
|
|
46
|
+
costUsd: record.costUsd,
|
|
47
|
+
durationMs: end - start,
|
|
48
|
+
model: record.model,
|
|
49
|
+
};
|
|
50
|
+
await sendWebhookNotification(payload);
|
|
167
51
|
}
|
|
168
52
|
//# sourceMappingURL=notify.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../../src/lib/relay/notify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../../src/lib/relay/notify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIhD,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,OAAuB;IACnE,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAC;IACnC,IAAI,CAAC,MAAM,EAAE,OAAO;QAAE,OAAO;IAE7B,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACtC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAgD,CAAC;YAAE,SAAS;QAEjG,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;YAClC,YAAY,EAAE,uBAAuB;YACrC,GAAG,OAAO,CAAC,OAAO;SACnB,CAAC;QAEF,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;YAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;YACrC,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC;iBAC1D,MAAM,CAAC,IAAI,CAAC;iBACZ,MAAM,CAAC,KAAK,CAAC,CAAC;YACjB,OAAO,CAAC,uBAAuB,CAAC,GAAG,UAAU,SAAS,EAAE,CAAC;QAC3D,CAAC;QAED,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;gBACvB,MAAM,EAAE,MAAM;gBACd,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;gBAC7B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC;aACpC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,wBAAwB,OAAO,CAAC,IAAI,aAAa,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrH,CAAC;IACH,CAAC;AACH,CAAC;AAED,sFAAsF;AACtF,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAAiB;IACzD,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC;IACnD,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,EAAE,CAAC;IAEnD,MAAM,OAAO,GAAmB;QAC9B,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,YAAY;QACrD,SAAS,EAAE,MAAM,CAAC,WAAW;QAC7B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,OAAO,IAAI,EAAE;QAC7C,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,UAAU,EAAE,GAAG,GAAG,KAAK;QACvB,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC;IAEF,MAAM,uBAAuB,CAAC,OAAO,CAAC,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface ChatMessage {
|
|
2
|
+
platform: string;
|
|
3
|
+
channelId: string;
|
|
4
|
+
conversationId: string;
|
|
5
|
+
messageId: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
userName: string;
|
|
8
|
+
text: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
isBot: boolean;
|
|
11
|
+
attachments?: ChatAttachment[];
|
|
12
|
+
}
|
|
13
|
+
export interface ChatAttachment {
|
|
14
|
+
type: string;
|
|
15
|
+
url: string;
|
|
16
|
+
content?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PlatformCapabilities {
|
|
19
|
+
nativeStreaming: boolean;
|
|
20
|
+
maxStreamUpdateHz: number;
|
|
21
|
+
supportsReactions: boolean;
|
|
22
|
+
supportsSlashCommands: boolean;
|
|
23
|
+
supportsThreads: boolean;
|
|
24
|
+
maxMessageLength: number;
|
|
25
|
+
}
|
|
26
|
+
export interface StreamHandle {
|
|
27
|
+
append(text: string): Promise<void>;
|
|
28
|
+
stop(finalText?: string): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export interface ChatPlatform {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly capabilities: PlatformCapabilities;
|
|
33
|
+
start(): Promise<void>;
|
|
34
|
+
stop(): Promise<void>;
|
|
35
|
+
sendMessage(channelId: string, text: string, threadId?: string): Promise<string>;
|
|
36
|
+
updateMessage(channelId: string, messageId: string, text: string): Promise<void>;
|
|
37
|
+
startStream(channelId: string, threadId?: string): Promise<StreamHandle>;
|
|
38
|
+
uploadFile(channelId: string, content: string, filename: string, threadId?: string): Promise<void>;
|
|
39
|
+
getThreadHistory(channelId: string, threadId: string, afterTs?: string): Promise<ChatMessage[]>;
|
|
40
|
+
onMessage: (msg: ChatMessage) => Promise<void>;
|
|
41
|
+
onReaction: (channelId: string, messageId: string, userId: string, emoji: string) => Promise<void>;
|
|
42
|
+
onCommand: (channelId: string, userId: string, command: string, args: string) => Promise<string>;
|
|
43
|
+
}
|
|
44
|
+
export interface FeedbackSignal {
|
|
45
|
+
type: 'positive' | 'negative' | 'acknowledge' | 'save';
|
|
46
|
+
platform: string;
|
|
47
|
+
conversationId: string;
|
|
48
|
+
messageId: string;
|
|
49
|
+
userId: string;
|
|
50
|
+
userName: string;
|
|
51
|
+
timestamp: string;
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../../../src/lib/relay/platform.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,6DAA6D;AAC7D,uDAAuD"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ChatPlatform, ChatMessage, PlatformCapabilities, StreamHandle } from '../platform.js';
|
|
2
|
+
export interface SlackAdapterConfig {
|
|
3
|
+
botToken: string;
|
|
4
|
+
appToken: string;
|
|
5
|
+
signingSecret: string;
|
|
6
|
+
mode: 'socket' | 'http';
|
|
7
|
+
port: number;
|
|
8
|
+
mainChannel?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class SlackAdapter implements ChatPlatform {
|
|
11
|
+
private config;
|
|
12
|
+
readonly name = "slack";
|
|
13
|
+
readonly capabilities: PlatformCapabilities;
|
|
14
|
+
onMessage: (msg: ChatMessage) => Promise<void>;
|
|
15
|
+
onReaction: (channelId: string, messageId: string, userId: string, emoji: string) => Promise<void>;
|
|
16
|
+
onCommand: (channelId: string, userId: string, command: string, args: string) => Promise<string>;
|
|
17
|
+
private app;
|
|
18
|
+
private botUserId;
|
|
19
|
+
private userCache;
|
|
20
|
+
private reconnectBackoff;
|
|
21
|
+
private lastEventTime;
|
|
22
|
+
private healthTimer;
|
|
23
|
+
private processedRetries;
|
|
24
|
+
constructor(config: SlackAdapterConfig);
|
|
25
|
+
start(): Promise<void>;
|
|
26
|
+
stop(): Promise<void>;
|
|
27
|
+
sendMessage(channelId: string, text: string, threadId?: string): Promise<string>;
|
|
28
|
+
updateMessage(channelId: string, messageId: string, text: string): Promise<void>;
|
|
29
|
+
startStream(channelId: string, threadId?: string): Promise<StreamHandle>;
|
|
30
|
+
uploadFile(channelId: string, content: string, filename: string, threadId?: string): Promise<void>;
|
|
31
|
+
getThreadHistory(channelId: string, threadId: string, afterTs?: string): Promise<ChatMessage[]>;
|
|
32
|
+
private shouldSkipRetry;
|
|
33
|
+
private resolveUserName;
|
|
34
|
+
private slackEventToChatMessage;
|
|
35
|
+
private slackMsgToChatMessage;
|
|
36
|
+
private attemptReconnect;
|
|
37
|
+
}
|