agentfeed 0.1.6 → 0.1.8

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,21 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { AgentFeedClient } from "../dist/api-client.js";
4
+ import { startMCPServer } from "../dist/mcp-server.js";
5
+
6
+ const serverUrl = process.env.AGENTFEED_BASE_URL?.replace(/\/api$/, "") || "http://localhost:3000";
7
+ const apiKey = process.env.AGENTFEED_API_KEY;
8
+
9
+ if (!apiKey) {
10
+ console.error("AGENTFEED_API_KEY environment variable is required");
11
+ process.exit(1);
12
+ }
13
+
14
+ const client = new AgentFeedClient(serverUrl, apiKey);
15
+
16
+ // Set agent ID if provided
17
+ if (process.env.AGENTFEED_AGENT_ID) {
18
+ client["_agentId"] = process.env.AGENTFEED_AGENT_ID;
19
+ }
20
+
21
+ startMCPServer({ client, serverUrl });
@@ -1,12 +1,17 @@
1
- import type { AgentInfo, FeedItem, FeedCommentItem, CommentItem, PaginatedResponse } from "./types.js";
1
+ import type { AgentInfo, FeedItem, FeedCommentItem, CommentItem, PostItem, PaginatedResponse } from "./types.js";
2
2
  export declare class AgentFeedClient {
3
- private baseUrl;
4
- private apiKey;
3
+ readonly baseUrl: string;
4
+ readonly apiKey: string;
5
+ private _agentId;
5
6
  constructor(baseUrl: string, apiKey: string);
7
+ get agentId(): string | undefined;
6
8
  private request;
7
- getMe(): Promise<AgentInfo>;
9
+ register(name: string): Promise<AgentInfo>;
8
10
  getSkillMd(): Promise<string>;
9
11
  getFeeds(): Promise<FeedItem[]>;
12
+ getFeedPosts(feedId: string, options?: {
13
+ limit?: number;
14
+ }): Promise<PaginatedResponse<PostItem>>;
10
15
  getFeedComments(feedId: string, options?: {
11
16
  author_type?: string;
12
17
  limit?: number;
@@ -16,4 +21,10 @@ export declare class AgentFeedClient {
16
21
  author_type?: string;
17
22
  limit?: number;
18
23
  }): Promise<PaginatedResponse<CommentItem>>;
24
+ setAgentStatus(params: {
25
+ status: "thinking" | "idle";
26
+ feed_id: string;
27
+ post_id: string;
28
+ }): Promise<void>;
29
+ reportSession(sessionName: string, claudeSessionId: string): Promise<void>;
19
30
  }
@@ -1,21 +1,41 @@
1
1
  export class AgentFeedClient {
2
2
  baseUrl;
3
3
  apiKey;
4
+ _agentId;
4
5
  constructor(baseUrl, apiKey) {
5
6
  this.baseUrl = baseUrl;
6
7
  this.apiKey = apiKey;
7
8
  }
8
- async request(path) {
9
+ get agentId() {
10
+ return this._agentId;
11
+ }
12
+ async request(path, options) {
13
+ const headers = {
14
+ Authorization: `Bearer ${this.apiKey}`,
15
+ };
16
+ if (this._agentId) {
17
+ headers["X-Agent-Id"] = this._agentId;
18
+ }
9
19
  const res = await fetch(`${this.baseUrl}${path}`, {
10
- headers: { Authorization: `Bearer ${this.apiKey}` },
20
+ ...options,
21
+ headers: {
22
+ ...headers,
23
+ ...options?.headers,
24
+ },
11
25
  });
12
26
  if (!res.ok) {
13
27
  throw new Error(`API error ${res.status}: ${await res.text()}`);
14
28
  }
15
29
  return res.json();
16
30
  }
17
- async getMe() {
18
- return this.request("/api/auth/me");
31
+ async register(name) {
32
+ const result = await this.request("/api/agents/register", {
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/json" },
35
+ body: JSON.stringify({ name }),
36
+ });
37
+ this._agentId = result.id;
38
+ return { id: result.id, name: result.name, type: "api" };
19
39
  }
20
40
  async getSkillMd() {
21
41
  const res = await fetch(`${this.baseUrl}/skill.md`);
@@ -27,6 +47,13 @@ export class AgentFeedClient {
27
47
  async getFeeds() {
28
48
  return this.request("/api/feeds");
29
49
  }
50
+ async getFeedPosts(feedId, options) {
51
+ const params = new URLSearchParams();
52
+ if (options?.limit)
53
+ params.set("limit", String(options.limit));
54
+ const qs = params.toString();
55
+ return this.request(`/api/feeds/${feedId}/posts${qs ? `?${qs}` : ""}`);
56
+ }
30
57
  async getFeedComments(feedId, options) {
31
58
  const params = new URLSearchParams();
32
59
  if (options?.author_type)
@@ -47,4 +74,27 @@ export class AgentFeedClient {
47
74
  const qs = params.toString();
48
75
  return this.request(`/api/posts/${postId}/comments${qs ? `?${qs}` : ""}`);
49
76
  }
77
+ async setAgentStatus(params) {
78
+ try {
79
+ await this.request("/api/agents/status", {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: JSON.stringify(params),
83
+ });
84
+ }
85
+ catch (err) {
86
+ // Non-critical: don't throw, just log
87
+ console.warn("Failed to set agent status:", err);
88
+ }
89
+ }
90
+ async reportSession(sessionName, claudeSessionId) {
91
+ await this.request("/api/agents/sessions", {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/json" },
94
+ body: JSON.stringify({
95
+ session_name: sessionName,
96
+ claude_session_id: claudeSessionId,
97
+ }),
98
+ });
99
+ }
50
100
  }
@@ -0,0 +1,10 @@
1
+ import { PersistentStore } from "./persistent-store.js";
2
+ export declare class FollowStore extends PersistentStore {
3
+ private posts;
4
+ constructor(filePath?: string);
5
+ protected serialize(): string;
6
+ protected deserialize(raw: string): void;
7
+ has(postId: string): boolean;
8
+ add(postId: string): void;
9
+ getAll(): string[];
10
+ }
@@ -0,0 +1,29 @@
1
+ import { PersistentStore } from "./persistent-store.js";
2
+ export class FollowStore extends PersistentStore {
3
+ posts = new Set();
4
+ constructor(filePath) {
5
+ super("followed-posts.json", filePath);
6
+ this.load();
7
+ }
8
+ serialize() {
9
+ return JSON.stringify(Array.from(this.posts), null, 2);
10
+ }
11
+ deserialize(raw) {
12
+ const data = JSON.parse(raw);
13
+ for (const id of data) {
14
+ this.posts.add(id);
15
+ }
16
+ }
17
+ has(postId) {
18
+ return this.posts.has(postId);
19
+ }
20
+ add(postId) {
21
+ if (this.posts.has(postId))
22
+ return;
23
+ this.posts.add(postId);
24
+ this.save();
25
+ }
26
+ getAll() {
27
+ return Array.from(this.posts);
28
+ }
29
+ }
package/dist/index.js CHANGED
@@ -1,9 +1,14 @@
1
+ import * as path from "node:path";
2
+ import * as readline from "node:readline";
1
3
  import { AgentFeedClient } from "./api-client.js";
2
4
  import { connectSSE } from "./sse-client.js";
3
5
  import { detectTrigger } from "./trigger.js";
4
6
  import { invokeAgent } from "./invoker.js";
5
7
  import { scanUnprocessed } from "./scanner.js";
6
8
  import { SessionStore } from "./session-store.js";
9
+ import { FollowStore } from "./follow-store.js";
10
+ import { QueueStore } from "./queue-store.js";
11
+ import { PostSessionStore } from "./post-session-store.js";
7
12
  const MAX_WAKE_ATTEMPTS = 3;
8
13
  const MAX_CRASH_RETRIES = 3;
9
14
  function getRequiredEnv(name) {
@@ -14,13 +19,59 @@ function getRequiredEnv(name) {
14
19
  }
15
20
  return value;
16
21
  }
22
+ function parsePermissionMode() {
23
+ const idx = process.argv.indexOf("--permission");
24
+ if (idx === -1)
25
+ return "safe";
26
+ const value = process.argv[idx + 1];
27
+ if (value === "yolo")
28
+ return "yolo";
29
+ if (value === "safe")
30
+ return "safe";
31
+ console.error(`Unknown permission mode: "${value}". Use "safe" (default) or "yolo".`);
32
+ process.exit(1);
33
+ }
34
+ function parseAllowedTools() {
35
+ const tools = [];
36
+ for (let i = 0; i < process.argv.length; i++) {
37
+ if (process.argv[i] === "--allowed-tools") {
38
+ // Collect all following args until the next flag (starts with --)
39
+ for (let j = i + 1; j < process.argv.length; j++) {
40
+ if (process.argv[j].startsWith("--"))
41
+ break;
42
+ tools.push(process.argv[j]);
43
+ }
44
+ break;
45
+ }
46
+ }
47
+ return tools;
48
+ }
49
+ function confirmYolo() {
50
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
51
+ return new Promise((resolve) => {
52
+ console.log("");
53
+ console.log(" \x1b[33m⚠️ YOLO mode enabled. The agent can do literally anything.\x1b[0m");
54
+ console.log(" \x1b[33m No prompt sandboxing. No trust boundaries.\x1b[0m");
55
+ console.log(" \x1b[33m Prompt injection? Not your problem today.\x1b[0m");
56
+ console.log("");
57
+ rl.question(" Continue? (y/N): ", (answer) => {
58
+ rl.close();
59
+ resolve(answer.trim().toLowerCase() === "y");
60
+ });
61
+ });
62
+ }
17
63
  const serverUrl = getRequiredEnv("AGENTFEED_URL");
18
64
  const apiKey = getRequiredEnv("AGENTFEED_API_KEY");
65
+ const permissionMode = parsePermissionMode();
66
+ const extraAllowedTools = parseAllowedTools();
19
67
  const client = new AgentFeedClient(serverUrl, apiKey);
20
68
  let isRunning = false;
21
69
  let sseConnection = null;
22
70
  const wakeAttempts = new Map();
23
71
  const sessionStore = new SessionStore(process.env.AGENTFEED_SESSION_FILE);
72
+ const followStore = new FollowStore(process.env.AGENTFEED_FOLLOW_FILE);
73
+ const queueStore = new QueueStore(process.env.AGENTFEED_QUEUE_FILE);
74
+ const postSessionStore = new PostSessionStore(process.env.AGENTFEED_POST_SESSION_FILE);
24
75
  function shutdown() {
25
76
  console.log("\nShutting down...");
26
77
  sseConnection?.close();
@@ -29,18 +80,29 @@ function shutdown() {
29
80
  process.on("SIGINT", shutdown);
30
81
  process.on("SIGTERM", shutdown);
31
82
  async function main() {
32
- console.log("AgentFeed Worker starting...");
33
- // Step 0: Initialize
34
- const agent = await client.getMe();
83
+ // Confirm yolo mode
84
+ if (permissionMode === "yolo") {
85
+ const confirmed = await confirmYolo();
86
+ if (!confirmed) {
87
+ console.log("Cancelled. Run without --permission yolo for safe mode.");
88
+ process.exit(0);
89
+ }
90
+ }
91
+ const toolsInfo = extraAllowedTools.length > 0
92
+ ? ` + ${extraAllowedTools.join(", ")}`
93
+ : "";
94
+ console.log(`AgentFeed Worker starting... (permission: ${permissionMode}${toolsInfo})`);
95
+ // Step 0: Register agent
96
+ const projectName = process.env.AGENTFEED_AGENT_NAME ?? path.basename(process.cwd());
97
+ const agent = await client.register(projectName);
35
98
  console.log(`Agent: ${agent.name} (${agent.id})`);
36
- const skillMd = await client.getSkillMd();
37
- console.log("Skill document cached.");
99
+ console.log("MCP mode enabled.");
38
100
  // Step 1: Startup scan for unprocessed items
39
101
  console.log("Scanning for unprocessed items...");
40
- const unprocessed = await scanUnprocessed(client, agent);
102
+ const unprocessed = await scanUnprocessed(client, agent, followStore, postSessionStore);
41
103
  if (unprocessed.length > 0) {
42
104
  console.log(`Found ${unprocessed.length} unprocessed item(s)`);
43
- await handleTriggers(unprocessed, agent, skillMd);
105
+ await handleTriggers(unprocessed, agent);
44
106
  }
45
107
  else {
46
108
  console.log("No unprocessed items found.");
@@ -48,14 +110,29 @@ async function main() {
48
110
  // Step 2: Connect to global SSE stream
49
111
  const sseUrl = `${serverUrl}/api/events/stream?author_type=human`;
50
112
  console.log("Connecting to global event stream...");
51
- sseConnection = connectSSE(sseUrl, apiKey, (rawEvent) => {
113
+ sseConnection = connectSSE(sseUrl, apiKey, client.agentId, (rawEvent) => {
52
114
  if (rawEvent.type === "heartbeat")
53
115
  return;
116
+ // Handle session_deleted events directly
117
+ if (rawEvent.type === "session_deleted") {
118
+ try {
119
+ const data = JSON.parse(rawEvent.data);
120
+ if (data.agent_id === client.agentId) {
121
+ sessionStore.delete(data.session_name);
122
+ postSessionStore.removeBySessionName(data.session_name);
123
+ console.log(`Session deleted: ${data.session_name}`);
124
+ }
125
+ }
126
+ catch (err) {
127
+ console.error("Failed to handle session_deleted event:", err);
128
+ }
129
+ return;
130
+ }
54
131
  try {
55
132
  const event = JSON.parse(rawEvent.data);
56
- const trigger = detectTrigger(event, agent);
133
+ const trigger = detectTrigger(event, agent, followStore, postSessionStore);
57
134
  if (trigger) {
58
- handleTriggers([trigger], agent, skillMd);
135
+ handleTriggers([trigger], agent);
59
136
  }
60
137
  }
61
138
  catch (err) {
@@ -66,73 +143,121 @@ async function main() {
66
143
  });
67
144
  console.log("Worker ready. Listening for events...");
68
145
  }
69
- async function handleTriggers(triggers, agent, skillMd) {
146
+ async function handleTriggers(triggers, agent) {
147
+ // Queue all incoming triggers (persisted to disk)
148
+ for (const t of triggers) {
149
+ queueStore.push(t);
150
+ console.log(`Queued trigger: ${t.triggerType} on ${t.postId} (queue size: ${queueStore.size})`);
151
+ }
70
152
  if (isRunning)
71
153
  return;
72
- // Filter by wake attempt limit
73
- const eligible = triggers.filter((t) => {
74
- const attempts = wakeAttempts.get(t.eventId) ?? 0;
75
- if (attempts >= MAX_WAKE_ATTEMPTS) {
76
- console.log(`Skipping ${t.eventId}: max wake attempts reached`);
77
- return false;
154
+ // Process queue until empty
155
+ await processQueue(agent);
156
+ }
157
+ async function processQueue(agent) {
158
+ while (true) {
159
+ const queued = queueStore.drain();
160
+ if (queued.length === 0)
161
+ break;
162
+ // Filter by wake attempt limit
163
+ const eligible = queued.filter((t) => {
164
+ const attempts = wakeAttempts.get(t.eventId) ?? 0;
165
+ if (attempts >= MAX_WAKE_ATTEMPTS) {
166
+ console.log(`Skipping ${t.eventId}: max wake attempts reached`);
167
+ return false;
168
+ }
169
+ return true;
170
+ });
171
+ if (eligible.length === 0)
172
+ break;
173
+ isRunning = true;
174
+ const trigger = eligible[0];
175
+ // Re-queue remaining items
176
+ for (const t of eligible.slice(1)) {
177
+ queueStore.push(t);
78
178
  }
79
- return true;
80
- });
81
- if (eligible.length === 0)
82
- return;
83
- isRunning = true;
84
- const trigger = eligible[0];
85
- wakeAttempts.set(trigger.eventId, (wakeAttempts.get(trigger.eventId) ?? 0) + 1);
86
- // Fetch recent context for the prompt
87
- const recentContext = await fetchContext(trigger);
88
- console.log(`Waking agent for: ${trigger.triggerType} on ${trigger.postId}`);
89
- let retries = 0;
90
- let success = false;
91
- while (retries < MAX_CRASH_RETRIES) {
179
+ wakeAttempts.set(trigger.eventId, (wakeAttempts.get(trigger.eventId) ?? 0) + 1);
180
+ // Auto-follow thread on mention (so future comments trigger without re-mention)
181
+ if (trigger.triggerType === "mention") {
182
+ followStore.add(trigger.postId);
183
+ console.log(`Following thread: ${trigger.postId}`);
184
+ }
185
+ // Fetch recent context for the prompt
186
+ const recentContext = await fetchContext(trigger);
187
+ console.log(`Waking agent for: ${trigger.triggerType} on ${trigger.postId} (session: ${trigger.sessionName})`);
188
+ // Report thinking status
189
+ await client.setAgentStatus({
190
+ status: "thinking",
191
+ feed_id: trigger.feedId,
192
+ post_id: trigger.postId,
193
+ });
194
+ let retries = 0;
195
+ let success = false;
92
196
  try {
93
- const result = await invokeAgent({
94
- agent,
95
- trigger,
96
- skillMd,
97
- apiKey,
98
- serverUrl,
99
- recentContext,
100
- sessionId: sessionStore.get(trigger.postId),
101
- });
102
- if (result.sessionId) {
103
- sessionStore.set(trigger.postId, result.sessionId);
197
+ while (retries < MAX_CRASH_RETRIES) {
198
+ try {
199
+ const result = await invokeAgent({
200
+ agent,
201
+ trigger,
202
+ apiKey,
203
+ serverUrl,
204
+ recentContext,
205
+ permissionMode,
206
+ extraAllowedTools,
207
+ sessionId: sessionStore.get(trigger.sessionName),
208
+ agentId: client.agentId,
209
+ });
210
+ if (result.sessionId) {
211
+ sessionStore.set(trigger.sessionName, result.sessionId);
212
+ postSessionStore.set(trigger.postId, trigger.sessionName);
213
+ // Report session to server
214
+ await client.reportSession(trigger.sessionName, result.sessionId).catch((err) => {
215
+ console.warn("Failed to report session:", err);
216
+ });
217
+ }
218
+ if (result.exitCode === 0) {
219
+ success = true;
220
+ break;
221
+ }
222
+ // If resume failed (stale session), clear it and retry as new session
223
+ if (result.exitCode !== 0 && sessionStore.get(trigger.sessionName)) {
224
+ console.log("Session may be stale, clearing and retrying as new session...");
225
+ sessionStore.delete(trigger.sessionName);
226
+ }
227
+ console.error(`Agent exited with code ${result.exitCode}, retry ${retries + 1}/${MAX_CRASH_RETRIES}`);
228
+ }
229
+ catch (err) {
230
+ console.error("Agent invocation error:", err);
231
+ }
232
+ retries++;
104
233
  }
105
- if (result.exitCode === 0) {
106
- success = true;
107
- break;
234
+ if (!success) {
235
+ console.error(`Agent failed after ${MAX_CRASH_RETRIES} retries`);
108
236
  }
109
- // If resume failed (stale session), clear it and retry as new session
110
- if (result.exitCode !== 0 && sessionStore.get(trigger.postId)) {
111
- console.log("Session may be stale, clearing and retrying as new session...");
112
- sessionStore.delete(trigger.postId);
237
+ }
238
+ finally {
239
+ // Always report idle when done
240
+ await client.setAgentStatus({
241
+ status: "idle",
242
+ feed_id: trigger.feedId,
243
+ post_id: trigger.postId,
244
+ });
245
+ }
246
+ isRunning = false;
247
+ // Post-completion: re-scan for items that arrived during execution and add to queue
248
+ try {
249
+ const newUnprocessed = await scanUnprocessed(client, agent, followStore, postSessionStore);
250
+ for (const t of newUnprocessed) {
251
+ queueStore.push(t);
252
+ }
253
+ if (newUnprocessed.length > 0) {
254
+ console.log(`Post-completion scan: ${newUnprocessed.length} item(s) added to queue`);
113
255
  }
114
- console.error(`Agent exited with code ${result.exitCode}, retry ${retries + 1}/${MAX_CRASH_RETRIES}`);
115
256
  }
116
257
  catch (err) {
117
- console.error("Agent invocation error:", err);
258
+ console.error("Post-completion scan error:", err);
118
259
  }
119
- retries++;
120
- }
121
- if (!success) {
122
- console.error(`Agent failed after ${MAX_CRASH_RETRIES} retries`);
123
- }
124
- isRunning = false;
125
- // Post-completion: re-scan for items that arrived during execution
126
- try {
127
- const agent2 = await client.getMe();
128
- const newUnprocessed = await scanUnprocessed(client, agent2);
129
- if (newUnprocessed.length > 0) {
130
- console.log(`Post-completion: ${newUnprocessed.length} unprocessed item(s) found`);
131
- await handleTriggers(newUnprocessed, agent, skillMd);
132
- }
133
- }
134
- catch (err) {
135
- console.error("Post-completion scan error:", err);
260
+ // Loop continues to process next item in queue
136
261
  }
137
262
  }
138
263
  async function fetchContext(trigger) {
package/dist/invoker.d.ts CHANGED
@@ -1,12 +1,14 @@
1
- import type { TriggerContext, AgentInfo } from "./types.js";
1
+ import type { TriggerContext, AgentInfo, PermissionMode } from "./types.js";
2
2
  interface InvokeOptions {
3
3
  agent: AgentInfo;
4
4
  trigger: TriggerContext;
5
- skillMd: string;
6
5
  apiKey: string;
7
6
  serverUrl: string;
8
7
  recentContext: string;
8
+ permissionMode: PermissionMode;
9
+ extraAllowedTools?: string[];
9
10
  sessionId?: string;
11
+ agentId?: string;
10
12
  }
11
13
  interface InvokeResult {
12
14
  exitCode: number;