agentfeed 0.1.7 → 0.1.10
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/bin/mcp-server.js +21 -0
- package/dist/agent-registry-store.d.ts +11 -0
- package/dist/agent-registry-store.js +31 -0
- package/dist/api-client.d.ts +11 -5
- package/dist/api-client.js +47 -6
- package/dist/backends/claude.d.ts +12 -0
- package/dist/backends/claude.js +92 -0
- package/dist/backends/codex.d.ts +13 -0
- package/dist/backends/codex.js +69 -0
- package/dist/backends/gemini.d.ts +11 -0
- package/dist/backends/gemini.js +102 -0
- package/dist/backends/index.d.ts +6 -0
- package/dist/backends/index.js +16 -0
- package/dist/backends/types.d.ts +23 -0
- package/dist/backends/types.js +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +129 -0
- package/dist/follow-store.d.ts +1 -0
- package/dist/follow-store.js +12 -0
- package/dist/index.js +157 -194
- package/dist/invoker.d.ts +6 -5
- package/dist/invoker.js +87 -60
- package/dist/mcp-server.d.ts +8 -0
- package/dist/mcp-server.js +230 -0
- package/dist/post-session-store.d.ts +20 -0
- package/dist/post-session-store.js +72 -0
- package/dist/processor.d.ts +21 -0
- package/dist/processor.js +168 -0
- package/dist/queue-store.js +2 -2
- package/dist/scanner.d.ts +3 -2
- package/dist/scanner.js +53 -25
- package/dist/session-store.d.ts +1 -0
- package/dist/session-store.js +3 -0
- package/dist/sse-client.d.ts +1 -1
- package/dist/sse-client.js +12 -4
- package/dist/trigger.d.ts +3 -2
- package/dist/trigger.js +82 -47
- package/dist/types.d.ts +24 -1
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +13 -3
- package/package.json +16 -2
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { invokeAgent } from "./invoker.js";
|
|
2
|
+
import { scanUnprocessed } from "./scanner.js";
|
|
3
|
+
const MAX_WAKE_ATTEMPTS = 3;
|
|
4
|
+
const MAX_CRASH_RETRIES = 3;
|
|
5
|
+
const AGENT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
|
+
const MAX_CONCURRENT = 5;
|
|
7
|
+
const MAX_BOT_MENTIONS_PER_POST = 4;
|
|
8
|
+
const wakeAttempts = new Map();
|
|
9
|
+
const botMentionCounts = new Map();
|
|
10
|
+
const runningKeys = new Set();
|
|
11
|
+
// Periodic cleanup to prevent memory growth
|
|
12
|
+
setInterval(() => { wakeAttempts.clear(); botMentionCounts.clear(); }, 10 * 60 * 1000);
|
|
13
|
+
function triggerKey(t) {
|
|
14
|
+
return `${t.backendType}:${t.sessionName}`;
|
|
15
|
+
}
|
|
16
|
+
export function handleTriggers(triggers, deps) {
|
|
17
|
+
for (const t of triggers) {
|
|
18
|
+
// Prevent bot-to-bot mention loops
|
|
19
|
+
if (t.authorIsBot && t.triggerType === "mention") {
|
|
20
|
+
const count = botMentionCounts.get(t.postId) ?? 0;
|
|
21
|
+
if (count >= MAX_BOT_MENTIONS_PER_POST) {
|
|
22
|
+
console.log(`Skipping bot mention on ${t.postId}: loop limit (${MAX_BOT_MENTIONS_PER_POST}) reached`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
botMentionCounts.set(t.postId, count + 1);
|
|
26
|
+
}
|
|
27
|
+
deps.queueStore.push(t);
|
|
28
|
+
console.log(`Queued trigger: ${t.triggerType} on ${t.postId} [${t.backendType}] (queue size: ${deps.queueStore.size})`);
|
|
29
|
+
}
|
|
30
|
+
scheduleQueue(deps);
|
|
31
|
+
}
|
|
32
|
+
function scheduleQueue(deps) {
|
|
33
|
+
const queued = deps.queueStore.drain();
|
|
34
|
+
if (queued.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const toRun = [];
|
|
37
|
+
for (const t of queued) {
|
|
38
|
+
const attempts = wakeAttempts.get(t.eventId) ?? 0;
|
|
39
|
+
if (attempts >= MAX_WAKE_ATTEMPTS) {
|
|
40
|
+
console.log(`Skipping ${t.eventId}: max wake attempts reached`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const key = triggerKey(t);
|
|
44
|
+
if (runningKeys.size >= MAX_CONCURRENT || runningKeys.has(key)) {
|
|
45
|
+
// Same backend+session already running — re-queue
|
|
46
|
+
deps.queueStore.push(t);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
toRun.push(t);
|
|
50
|
+
runningKeys.add(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const trigger of toRun) {
|
|
54
|
+
processItem(trigger, deps).catch((err) => {
|
|
55
|
+
console.error(`Error processing ${trigger.postId}:`, err);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function processItem(trigger, deps) {
|
|
60
|
+
const key = triggerKey(trigger);
|
|
61
|
+
wakeAttempts.set(trigger.eventId, (wakeAttempts.get(trigger.eventId) ?? 0) + 1);
|
|
62
|
+
const ba = deps.backendAgentMap.get(trigger.backendType);
|
|
63
|
+
if (!ba) {
|
|
64
|
+
console.warn(`No backend for ${trigger.backendType}, skipping`);
|
|
65
|
+
runningKeys.delete(key);
|
|
66
|
+
scheduleQueue(deps);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
// Auto-follow thread on mention
|
|
71
|
+
if (trigger.triggerType === "mention") {
|
|
72
|
+
deps.followStore.add(trigger.postId);
|
|
73
|
+
console.log(`Following thread: ${trigger.postId}`);
|
|
74
|
+
}
|
|
75
|
+
const sessionAgentId = await deps.ensureSessionAgent(trigger.sessionName, ba.agent.name, ba.backendType);
|
|
76
|
+
const recentContext = await fetchContext(trigger, deps);
|
|
77
|
+
console.log(`Waking agent for: ${trigger.triggerType} on ${trigger.postId} [${trigger.backendType}] (session: ${trigger.sessionName}, agentId: ${sessionAgentId})`);
|
|
78
|
+
await deps.client.setAgentStatus({
|
|
79
|
+
status: "thinking",
|
|
80
|
+
feed_id: trigger.feedId,
|
|
81
|
+
post_id: trigger.postId,
|
|
82
|
+
}, sessionAgentId);
|
|
83
|
+
// Server config overrides CLI args
|
|
84
|
+
const effectivePermission = ba.config?.permission_mode ?? deps.permissionMode;
|
|
85
|
+
const effectiveTools = ba.config?.allowed_tools?.length
|
|
86
|
+
? ba.config.allowed_tools
|
|
87
|
+
: deps.extraAllowedTools;
|
|
88
|
+
let retries = 0;
|
|
89
|
+
let success = false;
|
|
90
|
+
try {
|
|
91
|
+
while (retries < MAX_CRASH_RETRIES) {
|
|
92
|
+
try {
|
|
93
|
+
const result = await invokeAgent(ba.backend, {
|
|
94
|
+
agent: ba.agent,
|
|
95
|
+
trigger,
|
|
96
|
+
apiKey: deps.apiKey,
|
|
97
|
+
serverUrl: deps.serverUrl,
|
|
98
|
+
recentContext,
|
|
99
|
+
permissionMode: effectivePermission,
|
|
100
|
+
extraAllowedTools: effectiveTools,
|
|
101
|
+
sessionId: ba.sessionStore.get(trigger.sessionName),
|
|
102
|
+
agentId: sessionAgentId,
|
|
103
|
+
timeoutMs: AGENT_TIMEOUT_MS,
|
|
104
|
+
});
|
|
105
|
+
if (result.sessionId) {
|
|
106
|
+
ba.sessionStore.set(trigger.sessionName, result.sessionId);
|
|
107
|
+
deps.postSessionStore.set(trigger.postId, trigger.backendType, trigger.sessionName);
|
|
108
|
+
await deps.client.reportSession(trigger.sessionName, result.sessionId, sessionAgentId).catch((err) => {
|
|
109
|
+
console.warn("Failed to report session:", err);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (result.exitCode === 0) {
|
|
113
|
+
success = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (result.exitCode !== 0 && ba.sessionStore.get(trigger.sessionName)) {
|
|
117
|
+
console.log("Session may be stale, clearing and retrying as new session...");
|
|
118
|
+
ba.sessionStore.delete(trigger.sessionName);
|
|
119
|
+
}
|
|
120
|
+
console.error(`Agent exited with code ${result.exitCode}, retry ${retries + 1}/${MAX_CRASH_RETRIES}`);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
console.error("Agent invocation error:", err);
|
|
124
|
+
}
|
|
125
|
+
retries++;
|
|
126
|
+
}
|
|
127
|
+
if (!success) {
|
|
128
|
+
console.error(`Agent failed after ${MAX_CRASH_RETRIES} retries`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await deps.client.setAgentStatus({
|
|
133
|
+
status: "idle",
|
|
134
|
+
feed_id: trigger.feedId,
|
|
135
|
+
post_id: trigger.postId,
|
|
136
|
+
}, sessionAgentId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
runningKeys.delete(key);
|
|
141
|
+
// Post-completion: re-scan for items that arrived during execution
|
|
142
|
+
try {
|
|
143
|
+
const currentOwnIds = deps.agentRegistry.getAllIds();
|
|
144
|
+
const newUnprocessed = await scanUnprocessed(deps.client, deps.backendAgents, deps.followStore, deps.postSessionStore, currentOwnIds);
|
|
145
|
+
for (const t of newUnprocessed) {
|
|
146
|
+
deps.queueStore.push(t);
|
|
147
|
+
}
|
|
148
|
+
if (newUnprocessed.length > 0) {
|
|
149
|
+
console.log(`Post-completion scan: ${newUnprocessed.length} item(s) added to queue`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
console.error("Post-completion scan error:", err);
|
|
154
|
+
}
|
|
155
|
+
scheduleQueue(deps);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function fetchContext(trigger, deps) {
|
|
159
|
+
try {
|
|
160
|
+
const comments = await deps.client.getPostComments(trigger.postId, { limit: 10 });
|
|
161
|
+
return comments.data
|
|
162
|
+
.map((c) => `[${c.author_type}${c.author_name ? ` (${c.author_name})` : ""}] ${c.content}`)
|
|
163
|
+
.join("\n");
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
}
|
package/dist/queue-store.js
CHANGED
|
@@ -15,8 +15,8 @@ export class QueueStore extends PersistentStore {
|
|
|
15
15
|
// Deduplicate by eventId
|
|
16
16
|
if (this.queue.some((t) => t.eventId === trigger.eventId))
|
|
17
17
|
return;
|
|
18
|
-
// Deduplicate by postId — keep only the latest trigger per post
|
|
19
|
-
this.queue = this.queue.filter((t) => t.postId
|
|
18
|
+
// Deduplicate by (postId, backendType) — keep only the latest trigger per post per backend
|
|
19
|
+
this.queue = this.queue.filter((t) => !(t.postId === trigger.postId && t.backendType === trigger.backendType));
|
|
20
20
|
this.queue.push(trigger);
|
|
21
21
|
this.save();
|
|
22
22
|
}
|
package/dist/scanner.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TriggerContext, BackendAgent } from "./types.js";
|
|
2
2
|
import type { AgentFeedClient } from "./api-client.js";
|
|
3
3
|
import type { FollowStore } from "./follow-store.js";
|
|
4
|
-
|
|
4
|
+
import type { PostSessionStore } from "./post-session-store.js";
|
|
5
|
+
export declare function scanUnprocessed(client: AgentFeedClient, backendAgents: BackendAgent[], followStore?: FollowStore, postSessionStore?: PostSessionStore, ownAgentIds?: Set<string>): Promise<TriggerContext[]>;
|
package/dist/scanner.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export async function scanUnprocessed(client,
|
|
1
|
+
import { parseMention, isBotAuthor } from "./utils.js";
|
|
2
|
+
export async function scanUnprocessed(client, backendAgents, followStore, postSessionStore, ownAgentIds) {
|
|
3
3
|
const triggers = [];
|
|
4
4
|
const feeds = await client.getFeeds();
|
|
5
5
|
const processedPostIds = new Set();
|
|
6
|
+
const isOwnAgent = (id) => id !== null && (ownAgentIds ? ownAgentIds.has(id) : false);
|
|
7
|
+
const defaultBackendType = backendAgents[0]?.backendType ?? "claude";
|
|
6
8
|
for (const feed of feeds) {
|
|
7
9
|
// --- Scan comments (existing logic) ---
|
|
8
10
|
const comments = await client.getFeedComments(feed.id, {
|
|
@@ -20,23 +22,36 @@ export async function scanUnprocessed(client, agent, followStore) {
|
|
|
20
22
|
// Determine the best trigger type for this post
|
|
21
23
|
let bestTriggerType = null;
|
|
22
24
|
let bestComment = null;
|
|
25
|
+
let bestSessionName = "default";
|
|
26
|
+
let bestBackendType = defaultBackendType;
|
|
23
27
|
for (const comment of postComments) {
|
|
24
|
-
// Check for @mentions (highest priority)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
// Check for @mentions across all backend agents (highest priority)
|
|
29
|
+
for (const ba of backendAgents) {
|
|
30
|
+
const mention = parseMention(comment.content, ba.agent.name);
|
|
31
|
+
if (mention.mentioned) {
|
|
32
|
+
bestTriggerType = "mention";
|
|
33
|
+
bestComment = comment;
|
|
34
|
+
bestSessionName = mention.sessionName;
|
|
35
|
+
bestBackendType = ba.backendType;
|
|
36
|
+
}
|
|
28
37
|
}
|
|
29
38
|
// Check if this is on an agent-owned post
|
|
30
|
-
if (!bestTriggerType && comment.post_created_by
|
|
39
|
+
if (!bestTriggerType && isOwnAgent(comment.post_created_by)) {
|
|
40
|
+
const postSession = postSessionStore?.getWithType(postId);
|
|
31
41
|
bestTriggerType = "own_post_comment";
|
|
32
42
|
bestComment = comment;
|
|
43
|
+
bestSessionName = postSession?.sessionName ?? "default";
|
|
44
|
+
bestBackendType = postSession?.backendType ?? defaultBackendType;
|
|
33
45
|
}
|
|
34
46
|
}
|
|
35
47
|
// Check if this is in a followed thread
|
|
36
48
|
if (!bestTriggerType && followStore?.has(postId)) {
|
|
49
|
+
const postSession = postSessionStore?.getWithType(postId);
|
|
37
50
|
bestTriggerType = "thread_follow_up";
|
|
38
51
|
// Use the last human comment as the trigger
|
|
39
52
|
bestComment = postComments[postComments.length - 1] ?? null;
|
|
53
|
+
bestSessionName = postSession?.sessionName ?? "default";
|
|
54
|
+
bestBackendType = postSession?.backendType ?? defaultBackendType;
|
|
40
55
|
}
|
|
41
56
|
if (!bestTriggerType || !bestComment)
|
|
42
57
|
continue;
|
|
@@ -56,37 +71,50 @@ export async function scanUnprocessed(client, agent, followStore) {
|
|
|
56
71
|
postId,
|
|
57
72
|
content: bestComment.content,
|
|
58
73
|
authorName: bestComment.author_name,
|
|
74
|
+
authorIsBot: false,
|
|
75
|
+
sessionName: bestSessionName,
|
|
76
|
+
backendType: bestBackendType,
|
|
59
77
|
});
|
|
60
78
|
processedPostIds.add(postId);
|
|
61
79
|
}
|
|
62
80
|
}
|
|
63
|
-
// --- Scan post bodies for @mentions
|
|
81
|
+
// --- Scan post bodies for @mentions ---
|
|
64
82
|
const posts = await client.getFeedPosts(feed.id, { limit: 50 });
|
|
65
83
|
for (const post of posts.data) {
|
|
66
84
|
// Skip posts already handled by comment scan
|
|
67
85
|
if (processedPostIds.has(post.id))
|
|
68
86
|
continue;
|
|
69
87
|
// Skip bot-authored posts
|
|
70
|
-
if (post.created_by
|
|
88
|
+
if (isBotAuthor(post.created_by))
|
|
71
89
|
continue;
|
|
72
|
-
// Skip posts without content
|
|
73
|
-
if (!post.content
|
|
90
|
+
// Skip posts without content
|
|
91
|
+
if (!post.content)
|
|
74
92
|
continue;
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
feedId: feed.id,
|
|
85
|
-
feedName: feed.name,
|
|
86
|
-
postId: post.id,
|
|
87
|
-
content: post.content,
|
|
88
|
-
authorName: post.author_name,
|
|
93
|
+
// Try mentions across all backend agents
|
|
94
|
+
for (const ba of backendAgents) {
|
|
95
|
+
const mention = parseMention(post.content, ba.agent.name);
|
|
96
|
+
if (!mention.mentioned)
|
|
97
|
+
continue;
|
|
98
|
+
// Check if there's any bot reply on this post
|
|
99
|
+
const botReplies = await client.getPostComments(post.id, {
|
|
100
|
+
author_type: "bot",
|
|
101
|
+
limit: 1,
|
|
89
102
|
});
|
|
103
|
+
if (botReplies.data.length === 0) {
|
|
104
|
+
triggers.push({
|
|
105
|
+
triggerType: "mention",
|
|
106
|
+
eventId: post.id,
|
|
107
|
+
feedId: feed.id,
|
|
108
|
+
feedName: feed.name,
|
|
109
|
+
postId: post.id,
|
|
110
|
+
content: post.content,
|
|
111
|
+
authorName: post.author_name,
|
|
112
|
+
authorIsBot: false,
|
|
113
|
+
sessionName: mention.sessionName,
|
|
114
|
+
backendType: ba.backendType,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
break; // First matching backend wins for this post
|
|
90
118
|
}
|
|
91
119
|
}
|
|
92
120
|
}
|
package/dist/session-store.d.ts
CHANGED
package/dist/session-store.js
CHANGED
package/dist/sse-client.d.ts
CHANGED
|
@@ -6,4 +6,4 @@ export interface SSEEvent {
|
|
|
6
6
|
export interface SSEConnection {
|
|
7
7
|
close: () => void;
|
|
8
8
|
}
|
|
9
|
-
export declare function connectSSE(url: string, apiKey: string, onEvent: (event: SSEEvent) => void, onError: (error: Error) => void): SSEConnection;
|
|
9
|
+
export declare function connectSSE(url: string, apiKey: string, agentIds: string[], onEvent: (event: SSEEvent) => void, onError: (error: Error) => void): SSEConnection;
|
package/dist/sse-client.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { EventSource } from "eventsource";
|
|
2
|
-
const EVENT_TYPES = ["heartbeat", "post_created", "comment_created"];
|
|
2
|
+
const EVENT_TYPES = ["heartbeat", "post_created", "comment_created", "session_deleted"];
|
|
3
3
|
const BACKOFF_INITIAL_MS = 1_000;
|
|
4
4
|
const BACKOFF_MAX_MS = 60_000;
|
|
5
5
|
const BACKOFF_RESET_AFTER_MS = 30_000;
|
|
6
|
-
export function connectSSE(url, apiKey, onEvent, onError) {
|
|
6
|
+
export function connectSSE(url, apiKey, agentIds, onEvent, onError) {
|
|
7
7
|
let closed = false;
|
|
8
8
|
let currentEs = null;
|
|
9
9
|
let backoffMs = BACKOFF_INITIAL_MS;
|
|
@@ -14,10 +14,18 @@ export function connectSSE(url, apiKey, onEvent, onError) {
|
|
|
14
14
|
function connect() {
|
|
15
15
|
if (closed)
|
|
16
16
|
return;
|
|
17
|
-
|
|
17
|
+
// Pass all agent IDs as query parameter for multi-agent online tracking
|
|
18
|
+
const connectUrl = new URL(url);
|
|
19
|
+
if (agentIds.length > 0) {
|
|
20
|
+
connectUrl.searchParams.set("agents", agentIds.join(","));
|
|
21
|
+
}
|
|
22
|
+
const es = new EventSource(connectUrl.toString(), {
|
|
18
23
|
fetch: (input, init) => fetch(input, {
|
|
19
24
|
...init,
|
|
20
|
-
headers: {
|
|
25
|
+
headers: {
|
|
26
|
+
...init.headers,
|
|
27
|
+
Authorization: `Bearer ${apiKey}`,
|
|
28
|
+
},
|
|
21
29
|
}),
|
|
22
30
|
});
|
|
23
31
|
currentEs = es;
|
package/dist/trigger.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { FollowStore } from "./follow-store.js";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type { PostSessionStore } from "./post-session-store.js";
|
|
3
|
+
import type { GlobalEvent, TriggerContext, BackendAgent } from "./types.js";
|
|
4
|
+
export declare function detectTriggers(event: GlobalEvent, backendAgents: BackendAgent[], followStore?: FollowStore, postSessionStore?: PostSessionStore, ownAgentIds?: Set<string>): TriggerContext[];
|
package/dist/trigger.js
CHANGED
|
@@ -1,56 +1,91 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export function
|
|
1
|
+
import { parseMention, isBotAuthor } from "./utils.js";
|
|
2
|
+
export function detectTriggers(event, backendAgents, followStore, postSessionStore, ownAgentIds) {
|
|
3
|
+
const isOwnAgent = (id) => id !== null && (ownAgentIds ? ownAgentIds.has(id) : false);
|
|
4
|
+
const defaultBackendType = backendAgents[0]?.backendType ?? "claude";
|
|
3
5
|
if (event.type === "comment_created") {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
const authorIsOwnBot = isOwnAgent(event.created_by);
|
|
7
|
+
// Trigger 1: Comment on own post (human-authored only — prevents bot loop)
|
|
8
|
+
if (!authorIsOwnBot && isOwnAgent(event.post_created_by)) {
|
|
9
|
+
const postSession = postSessionStore?.getWithType(event.post_id);
|
|
10
|
+
return [{
|
|
11
|
+
triggerType: "own_post_comment",
|
|
12
|
+
eventId: event.id,
|
|
13
|
+
feedId: event.feed_id,
|
|
14
|
+
feedName: "",
|
|
15
|
+
postId: event.post_id,
|
|
16
|
+
content: event.content,
|
|
17
|
+
authorName: event.author_name,
|
|
18
|
+
authorIsBot: event.author_type === "bot",
|
|
19
|
+
sessionName: postSession?.sessionName ?? "default",
|
|
20
|
+
backendType: postSession?.backendType ?? defaultBackendType,
|
|
21
|
+
}];
|
|
15
22
|
}
|
|
16
|
-
// Trigger 2: @mention in comment
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
// Trigger 2: @mention in comment — collect ALL matching agents
|
|
24
|
+
const mentions = [];
|
|
25
|
+
for (const ba of backendAgents) {
|
|
26
|
+
if (event.created_by === ba.agent.id)
|
|
27
|
+
continue; // skip self-mention
|
|
28
|
+
const mention = parseMention(event.content, ba.agent.name);
|
|
29
|
+
if (mention.mentioned) {
|
|
30
|
+
mentions.push({
|
|
31
|
+
triggerType: "mention",
|
|
32
|
+
eventId: `${event.id}:${ba.backendType}`,
|
|
33
|
+
feedId: event.feed_id,
|
|
34
|
+
feedName: "",
|
|
35
|
+
postId: event.post_id,
|
|
36
|
+
content: event.content,
|
|
37
|
+
authorName: event.author_name,
|
|
38
|
+
authorIsBot: event.author_type === "bot",
|
|
39
|
+
sessionName: mention.sessionName,
|
|
40
|
+
backendType: ba.backendType,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
27
43
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
if (mentions.length > 0)
|
|
45
|
+
return mentions;
|
|
46
|
+
// Trigger 3: Comment in a followed thread (human-authored only — prevents bot loop)
|
|
47
|
+
if (!authorIsOwnBot && followStore?.has(event.post_id)) {
|
|
48
|
+
const postSession = postSessionStore?.getWithType(event.post_id);
|
|
49
|
+
return [{
|
|
50
|
+
triggerType: "thread_follow_up",
|
|
51
|
+
eventId: event.id,
|
|
52
|
+
feedId: event.feed_id,
|
|
53
|
+
feedName: "",
|
|
54
|
+
postId: event.post_id,
|
|
55
|
+
content: event.content,
|
|
56
|
+
authorName: event.author_name,
|
|
57
|
+
authorIsBot: event.author_type === "bot",
|
|
58
|
+
sessionName: postSession?.sessionName ?? "default",
|
|
59
|
+
backendType: postSession?.backendType ?? defaultBackendType,
|
|
60
|
+
}];
|
|
39
61
|
}
|
|
40
62
|
}
|
|
41
63
|
if (event.type === "post_created") {
|
|
42
|
-
// @mention in post content
|
|
43
|
-
if (event.content
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
// @mention in post content — collect ALL matching agents
|
|
65
|
+
if (event.content) {
|
|
66
|
+
const mentions = [];
|
|
67
|
+
for (const ba of backendAgents) {
|
|
68
|
+
if (event.created_by === ba.agent.id)
|
|
69
|
+
continue;
|
|
70
|
+
const mention = parseMention(event.content, ba.agent.name);
|
|
71
|
+
if (mention.mentioned) {
|
|
72
|
+
mentions.push({
|
|
73
|
+
triggerType: "mention",
|
|
74
|
+
eventId: `${event.id}:${ba.backendType}`,
|
|
75
|
+
feedId: event.feed_id,
|
|
76
|
+
feedName: event.feed_name,
|
|
77
|
+
postId: event.id,
|
|
78
|
+
content: event.content,
|
|
79
|
+
authorName: event.author_name,
|
|
80
|
+
authorIsBot: isBotAuthor(event.created_by),
|
|
81
|
+
sessionName: mention.sessionName,
|
|
82
|
+
backendType: ba.backendType,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (mentions.length > 0)
|
|
87
|
+
return mentions;
|
|
53
88
|
}
|
|
54
89
|
}
|
|
55
|
-
return
|
|
90
|
+
return [];
|
|
56
91
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
import type { CLIBackend } from "./backends/types.js";
|
|
2
|
+
import type { SessionStore } from "./session-store.js";
|
|
1
3
|
export interface AgentInfo {
|
|
2
4
|
id: string;
|
|
3
5
|
name: string;
|
|
4
6
|
type: string;
|
|
5
7
|
}
|
|
8
|
+
export interface AgentConfig {
|
|
9
|
+
permission_mode: PermissionMode;
|
|
10
|
+
allowed_tools: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface BackendAgent {
|
|
13
|
+
backendType: BackendType;
|
|
14
|
+
backend: CLIBackend;
|
|
15
|
+
agent: AgentInfo;
|
|
16
|
+
sessionStore: SessionStore;
|
|
17
|
+
config?: AgentConfig;
|
|
18
|
+
}
|
|
6
19
|
export interface GlobalPostEvent {
|
|
7
20
|
type: "post_created";
|
|
8
21
|
id: string;
|
|
@@ -25,7 +38,13 @@ export interface GlobalCommentEvent {
|
|
|
25
38
|
created_at: string;
|
|
26
39
|
post_created_by: string | null;
|
|
27
40
|
}
|
|
28
|
-
export
|
|
41
|
+
export interface GlobalSessionDeletedEvent {
|
|
42
|
+
type: "session_deleted";
|
|
43
|
+
agent_id: string;
|
|
44
|
+
agent_name: string;
|
|
45
|
+
session_name: string;
|
|
46
|
+
}
|
|
47
|
+
export type GlobalEvent = GlobalPostEvent | GlobalCommentEvent | GlobalSessionDeletedEvent;
|
|
29
48
|
export interface TriggerContext {
|
|
30
49
|
triggerType: "own_post_comment" | "mention" | "thread_follow_up";
|
|
31
50
|
eventId: string;
|
|
@@ -34,6 +53,9 @@ export interface TriggerContext {
|
|
|
34
53
|
postId: string;
|
|
35
54
|
content: string;
|
|
36
55
|
authorName: string | null;
|
|
56
|
+
authorIsBot: boolean;
|
|
57
|
+
sessionName: string;
|
|
58
|
+
backendType: BackendType;
|
|
37
59
|
}
|
|
38
60
|
export interface FeedItem {
|
|
39
61
|
id: string;
|
|
@@ -73,3 +95,4 @@ export interface PaginatedResponse<T> {
|
|
|
73
95
|
has_more: boolean;
|
|
74
96
|
}
|
|
75
97
|
export type PermissionMode = "safe" | "yolo";
|
|
98
|
+
export type BackendType = "claude" | "codex" | "gemini";
|
package/dist/utils.d.ts
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
+
export interface MentionMatch {
|
|
2
|
+
mentioned: boolean;
|
|
3
|
+
sessionName: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function parseMention(text: string, agentName: string): MentionMatch;
|
|
1
6
|
export declare function containsMention(text: string, agentName: string): boolean;
|
|
7
|
+
/** Check if created_by belongs to a bot (API key or registered agent) */
|
|
8
|
+
export declare function isBotAuthor(createdBy: string | null | undefined): boolean;
|
package/dist/utils.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function parseMention(text, agentName) {
|
|
2
2
|
const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3
|
-
const regex = new RegExp(`@${escaped}
|
|
4
|
-
|
|
3
|
+
const regex = new RegExp(`@${escaped}(?:/([a-zA-Z0-9_-]+))?\\b`, "i");
|
|
4
|
+
const match = regex.exec(text);
|
|
5
|
+
if (!match)
|
|
6
|
+
return { mentioned: false, sessionName: "default" };
|
|
7
|
+
return { mentioned: true, sessionName: match[1] ?? "default" };
|
|
8
|
+
}
|
|
9
|
+
export function containsMention(text, agentName) {
|
|
10
|
+
return parseMention(text, agentName).mentioned;
|
|
11
|
+
}
|
|
12
|
+
/** Check if created_by belongs to a bot (API key or registered agent) */
|
|
13
|
+
export function isBotAuthor(createdBy) {
|
|
14
|
+
return createdBy?.startsWith("af_") === true || createdBy?.startsWith("ag_") === true;
|
|
5
15
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentfeed",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Worker daemon for AgentFeed - watches feeds and wakes AI agents via claude -p",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,20 @@
|
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc",
|
|
15
|
-
"dev": "tsx src/index.ts"
|
|
15
|
+
"dev": "tsx --env-file=../../.env.local src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ai-agent",
|
|
19
|
+
"feed",
|
|
20
|
+
"claude",
|
|
21
|
+
"mcp",
|
|
22
|
+
"sse"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/daige-st/agentfeed.git",
|
|
28
|
+
"directory": "packages/worker"
|
|
16
29
|
},
|
|
17
30
|
"engines": {
|
|
18
31
|
"node": ">=18"
|
|
@@ -23,6 +36,7 @@
|
|
|
23
36
|
"typescript": "^5"
|
|
24
37
|
},
|
|
25
38
|
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
26
40
|
"eventsource": "^4.1.0"
|
|
27
41
|
}
|
|
28
42
|
}
|