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.
@@ -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
+ }
@@ -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 !== trigger.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 { AgentInfo, TriggerContext } from "./types.js";
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
- export declare function scanUnprocessed(client: AgentFeedClient, agent: AgentInfo, followStore?: FollowStore): Promise<TriggerContext[]>;
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 { containsMention } from "./utils.js";
2
- export async function scanUnprocessed(client, agent, followStore) {
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
- if (containsMention(comment.content, agent.name)) {
26
- bestTriggerType = "mention";
27
- bestComment = comment;
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 === agent.id) {
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 (new logic) ---
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.startsWith("af_"))
88
+ if (isBotAuthor(post.created_by))
71
89
  continue;
72
- // Skip posts without content or without @mention
73
- if (!post.content || !containsMention(post.content, agent.name))
90
+ // Skip posts without content
91
+ if (!post.content)
74
92
  continue;
75
- // Check if there's any bot reply on this post
76
- const botReplies = await client.getPostComments(post.id, {
77
- author_type: "bot",
78
- limit: 1,
79
- });
80
- if (botReplies.data.length === 0) {
81
- triggers.push({
82
- triggerType: "mention",
83
- eventId: post.id,
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
  }
@@ -7,4 +7,5 @@ export declare class SessionStore extends PersistentStore {
7
7
  get(postId: string): string | undefined;
8
8
  set(postId: string, sessionId: string): void;
9
9
  delete(postId: string): void;
10
+ keys(): string[];
10
11
  }
@@ -25,4 +25,7 @@ export class SessionStore extends PersistentStore {
25
25
  this.map.delete(postId);
26
26
  this.save();
27
27
  }
28
+ keys() {
29
+ return Array.from(this.map.keys());
30
+ }
28
31
  }
@@ -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;
@@ -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
- const es = new EventSource(url, {
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: { ...init.headers, Authorization: `Bearer ${apiKey}` },
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 { GlobalEvent, TriggerContext, AgentInfo } from "./types.js";
3
- export declare function detectTrigger(event: GlobalEvent, agent: AgentInfo, followStore?: FollowStore): TriggerContext | null;
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 { containsMention } from "./utils.js";
2
- export function detectTrigger(event, agent, followStore) {
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
- // Trigger 1: Comment on own post
5
- if (event.post_created_by === agent.id) {
6
- return {
7
- triggerType: "own_post_comment",
8
- eventId: event.id,
9
- feedId: event.feed_id,
10
- feedName: "",
11
- postId: event.post_id,
12
- content: event.content,
13
- authorName: event.author_name,
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
- if (containsMention(event.content, agent.name)) {
18
- return {
19
- triggerType: "mention",
20
- eventId: event.id,
21
- feedId: event.feed_id,
22
- feedName: "",
23
- postId: event.post_id,
24
- content: event.content,
25
- authorName: event.author_name,
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
- // Trigger 3: Comment in a followed thread
29
- if (followStore?.has(event.post_id)) {
30
- return {
31
- triggerType: "thread_follow_up",
32
- eventId: event.id,
33
- feedId: event.feed_id,
34
- feedName: "",
35
- postId: event.post_id,
36
- content: event.content,
37
- authorName: event.author_name,
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 && containsMention(event.content, agent.name)) {
44
- return {
45
- triggerType: "mention",
46
- eventId: event.id,
47
- feedId: event.feed_id,
48
- feedName: event.feed_name,
49
- postId: event.id,
50
- content: event.content,
51
- authorName: event.author_name,
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 null;
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 type GlobalEvent = GlobalPostEvent | GlobalCommentEvent;
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 containsMention(text, agentName) {
1
+ export function parseMention(text, agentName) {
2
2
  const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3
- const regex = new RegExp(`@${escaped}\\b`, "i");
4
- return regex.test(text);
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.7",
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
  }