agentfeed 0.1.11 → 0.1.12

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.
@@ -24,7 +24,7 @@ export class ClaudeBackend {
24
24
  fs.writeFileSync(this.mcpConfigPath, JSON.stringify(config, null, 2));
25
25
  }
26
26
  buildArgs(options) {
27
- const { prompt, systemPrompt, sessionId, permissionMode, extraAllowedTools, model } = options;
27
+ const { prompt, systemPrompt, sessionId, permissionMode, extraAllowedTools, model, chrome } = options;
28
28
  const args = [
29
29
  "-p", prompt,
30
30
  "--append-system-prompt", systemPrompt,
@@ -33,11 +33,17 @@ export class ClaudeBackend {
33
33
  if (model) {
34
34
  args.push("--model", model);
35
35
  }
36
+ if (chrome) {
37
+ args.push("--chrome");
38
+ }
36
39
  if (permissionMode === "yolo") {
37
40
  args.push("--dangerously-skip-permissions");
38
41
  }
39
42
  else {
40
43
  const allowedTools = ["mcp__agentfeed__*", ...(extraAllowedTools ?? [])];
44
+ if (chrome) {
45
+ allowedTools.push("mcp__claude-in-chrome__*");
46
+ }
41
47
  for (const tool of allowedTools) {
42
48
  args.push("--allowedTools", tool);
43
49
  }
@@ -7,6 +7,7 @@ export interface BuildArgsOptions {
7
7
  permissionMode: PermissionMode;
8
8
  extraAllowedTools?: string[];
9
9
  model?: string;
10
+ chrome?: boolean;
10
11
  }
11
12
  export interface CLIBackend {
12
13
  readonly name: BackendType;
package/dist/cli.js CHANGED
@@ -100,6 +100,16 @@ export function probeBackend(type) {
100
100
  });
101
101
  }
102
102
  export function confirmYolo() {
103
+ // --yes flag skips the interactive confirmation
104
+ if (process.argv.includes("--yes") || process.argv.includes("-y")) {
105
+ console.log("");
106
+ console.log(" \x1b[33m⚠️ YOLO mode enabled. The agent can do literally anything.\x1b[0m");
107
+ console.log(" \x1b[33m No prompt sandboxing. No trust boundaries.\x1b[0m");
108
+ console.log(" \x1b[33m Prompt injection? Not your problem today.\x1b[0m");
109
+ console.log("");
110
+ console.log(" Continue? (y/N): y (auto-confirmed via --yes)");
111
+ return Promise.resolve(true);
112
+ }
103
113
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
104
114
  return new Promise((resolve) => {
105
115
  console.log("");
package/dist/invoker.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface InvokeOptions {
9
9
  permissionMode: PermissionMode;
10
10
  extraAllowedTools?: string[];
11
11
  model?: string;
12
+ chrome?: boolean;
12
13
  sessionId?: string;
13
14
  agentId?: string;
14
15
  timeoutMs?: number;
@@ -16,5 +17,6 @@ export interface InvokeOptions {
16
17
  export interface InvokeResult {
17
18
  exitCode: number;
18
19
  sessionId?: string;
20
+ timedOut: boolean;
19
21
  }
20
22
  export declare function invokeAgent(backend: CLIBackend, options: InvokeOptions): Promise<InvokeResult>;
package/dist/invoker.js CHANGED
@@ -42,6 +42,7 @@ export function invokeAgent(backend, options) {
42
42
  permissionMode: options.permissionMode,
43
43
  extraAllowedTools: options.extraAllowedTools,
44
44
  model: options.model,
45
+ chrome: options.chrome,
45
46
  });
46
47
  const env = backend.buildEnv({
47
48
  ...agentfeedEnv,
@@ -55,12 +56,14 @@ export function invokeAgent(backend, options) {
55
56
  console.log(`Invoking ${backend.name}...`);
56
57
  const child = spawn(backend.binaryName, args, {
57
58
  env,
58
- stdio: isNewSession ? ["inherit", "pipe", "inherit"] : "inherit",
59
+ stdio: isNewSession ? ["ignore", "pipe", "inherit"] : ["ignore", "inherit", "inherit"],
59
60
  });
60
61
  // Timeout watchdog
61
62
  let killTimer = null;
63
+ let timedOut = false;
62
64
  if (options.timeoutMs) {
63
65
  killTimer = setTimeout(() => {
66
+ timedOut = true;
64
67
  console.warn(`Agent timed out after ${options.timeoutMs / 1000}s, killing process...`);
65
68
  child.kill("SIGTERM");
66
69
  setTimeout(() => { if (!child.killed)
@@ -104,7 +107,7 @@ export function invokeAgent(backend, options) {
104
107
  if (isNewSession)
105
108
  process.stdout.write("\n");
106
109
  console.log(`Agent exited (code ${code ?? "unknown"})`);
107
- resolve({ exitCode: code ?? 1, sessionId: sessionId ?? options.sessionId });
110
+ resolve({ exitCode: code ?? 1, sessionId: sessionId ?? options.sessionId, timedOut });
108
111
  });
109
112
  });
110
113
  }
@@ -138,7 +141,15 @@ function buildSystemPrompt(options) {
138
141
  - agentfeed_create_post - Create a new post in a feed
139
142
  - agentfeed_get_comments - Get comments on a post (use since/author_type filters)
140
143
  - agentfeed_post_comment - Post a comment (Korean and emoji supported!)
141
- - agentfeed_download_file - Download and view uploaded files (images, etc.)${statusTool}`;
144
+ - agentfeed_download_file - Download and view uploaded files (images, etc.)
145
+ - agentfeed_upload_file - Upload a local file and get markdown URL${statusTool}`;
146
+ const chromeSection = options.chrome
147
+ ? `\n\n# Chrome Browser
148
+
149
+ You have Chrome browser automation tools (mcp__claude-in-chrome__*) available.
150
+ Use these to navigate web pages, take screenshots, and interact with browser content.
151
+ To capture and share a web page: take a screenshot, save to /tmp, then upload via agentfeed_upload_file.`
152
+ : "";
142
153
  const imageGuidance = `IMPORTANT: When content contains image URLs like ![name](/api/uploads/up_xxx.png), use agentfeed_download_file to view the image before responding about it.`;
143
154
  if (options.permissionMode === "yolo") {
144
155
  return `# AgentFeed
@@ -149,7 +160,7 @@ ${toolList}
149
160
 
150
161
  Use these tools to interact with the feed. All content encoding is handled automatically.
151
162
 
152
- ${imageGuidance}`;
163
+ ${imageGuidance}${chromeSection}`;
153
164
  }
154
165
  return `${SECURITY_POLICY}
155
166
 
@@ -161,7 +172,7 @@ ${toolList}
161
172
 
162
173
  Use these tools to interact with the feed. All content encoding is handled automatically.
163
174
 
164
- ${imageGuidance}`;
175
+ ${imageGuidance}${chromeSection}`;
165
176
  }
166
177
  function wrapUntrusted(text) {
167
178
  return `<untrusted_content>\n${escapeXml(text)}\n</untrusted_content>`;
@@ -1,3 +1,5 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename } from "node:path";
1
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
5
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
@@ -82,6 +84,22 @@ const TOOLS = [
82
84
  required: ["url"],
83
85
  },
84
86
  },
87
+ {
88
+ name: "agentfeed_upload_file",
89
+ description: "Upload a file to AgentFeed. Returns the URL that can be used in markdown. " +
90
+ "For images: ![alt](/api/uploads/up_xxx.png), for others: [filename](/api/uploads/up_xxx.ext). " +
91
+ "Use this to share generated charts, images, or any files in posts and comments.",
92
+ inputSchema: {
93
+ type: "object",
94
+ properties: {
95
+ file_path: {
96
+ type: "string",
97
+ description: "Absolute path to the file to upload (e.g. /tmp/chart.svg)",
98
+ },
99
+ },
100
+ required: ["file_path"],
101
+ },
102
+ },
85
103
  {
86
104
  name: "agentfeed_set_status",
87
105
  description: "Report agent status (thinking/idle)",
@@ -184,6 +202,56 @@ async function handleToolCall(name, args, ctx) {
184
202
  ],
185
203
  };
186
204
  }
205
+ case "agentfeed_upload_file": {
206
+ const { file_path } = args;
207
+ const fileBuffer = await readFile(file_path);
208
+ const fileName = basename(file_path);
209
+ const ext = fileName.includes(".") ? fileName.split(".").pop().toLowerCase() : "";
210
+ const mimeTypes = {
211
+ svg: "image/svg+xml",
212
+ png: "image/png",
213
+ jpg: "image/jpeg",
214
+ jpeg: "image/jpeg",
215
+ gif: "image/gif",
216
+ webp: "image/webp",
217
+ pdf: "application/pdf",
218
+ json: "application/json",
219
+ csv: "text/csv",
220
+ txt: "text/plain",
221
+ md: "text/markdown",
222
+ html: "text/html",
223
+ mp4: "video/mp4",
224
+ webm: "video/webm",
225
+ };
226
+ const mimeType = mimeTypes[ext] || "application/octet-stream";
227
+ const blob = new Blob([fileBuffer], { type: mimeType });
228
+ const formData = new FormData();
229
+ formData.append("file", blob, fileName);
230
+ const uploadRes = await fetch(`${serverUrl}/api/uploads`, {
231
+ method: "POST",
232
+ headers: {
233
+ Authorization: `Bearer ${client.apiKey}`,
234
+ Origin: serverUrl,
235
+ ...(client.agentId ? { "X-Agent-Id": client.agentId } : {}),
236
+ },
237
+ body: formData,
238
+ });
239
+ if (!uploadRes.ok)
240
+ throw new Error(`Failed to upload file: ${uploadRes.status} ${await uploadRes.text()}`);
241
+ const upload = (await uploadRes.json());
242
+ const isImage = upload.mime_type.startsWith("image/");
243
+ const markdown = isImage
244
+ ? `![${fileName}](${upload.url})`
245
+ : `[${fileName}](${upload.url})`;
246
+ return {
247
+ content: [
248
+ {
249
+ type: "text",
250
+ text: `File uploaded successfully.\nURL: ${upload.url}\nMarkdown: ${markdown}\nSize: ${upload.size} bytes`,
251
+ },
252
+ ],
253
+ };
254
+ }
187
255
  case "agentfeed_set_status": {
188
256
  const { status, feed_id, post_id } = args;
189
257
  await client.setAgentStatus({ status, feed_id, post_id });
package/dist/processor.js CHANGED
@@ -8,6 +8,8 @@ const MAX_BOT_MENTIONS_PER_POST = 4;
8
8
  const wakeAttempts = new Map();
9
9
  const botMentionCounts = new Map();
10
10
  const runningKeys = new Set();
11
+ let retryTimer = null;
12
+ const RETRY_DELAY_MS = 3000;
11
13
  // Periodic cleanup to prevent memory growth
12
14
  setInterval(() => { wakeAttempts.clear(); botMentionCounts.clear(); }, 10 * 60 * 1000);
13
15
  function triggerKey(t) {
@@ -34,6 +36,7 @@ function scheduleQueue(deps) {
34
36
  if (queued.length === 0)
35
37
  return;
36
38
  const toRun = [];
39
+ let hasRequeued = false;
37
40
  for (const t of queued) {
38
41
  const attempts = wakeAttempts.get(t.eventId) ?? 0;
39
42
  if (attempts >= MAX_WAKE_ATTEMPTS) {
@@ -44,12 +47,22 @@ function scheduleQueue(deps) {
44
47
  if (runningKeys.size >= MAX_CONCURRENT || runningKeys.has(key)) {
45
48
  // Same backend+session already running — re-queue
46
49
  deps.queueStore.push(t);
50
+ hasRequeued = true;
47
51
  }
48
52
  else {
49
53
  toRun.push(t);
50
54
  runningKeys.add(key);
51
55
  }
52
56
  }
57
+ // Schedule retry for re-queued items so they don't wait for next SSE event
58
+ if (hasRequeued) {
59
+ if (retryTimer)
60
+ clearTimeout(retryTimer);
61
+ retryTimer = setTimeout(() => {
62
+ retryTimer = null;
63
+ scheduleQueue(deps);
64
+ }, RETRY_DELAY_MS);
65
+ }
53
66
  for (const trigger of toRun) {
54
67
  processItem(trigger, deps).catch((err) => {
55
68
  console.error(`Error processing ${trigger.postId}:`, err);
@@ -86,6 +99,7 @@ async function processItem(trigger, deps) {
86
99
  ? ba.config.allowed_tools
87
100
  : deps.extraAllowedTools;
88
101
  const effectiveModel = ba.config?.model;
102
+ const effectiveChrome = ba.config?.chrome ?? false;
89
103
  let retries = 0;
90
104
  let success = false;
91
105
  try {
@@ -100,6 +114,7 @@ async function processItem(trigger, deps) {
100
114
  permissionMode: effectivePermission,
101
115
  extraAllowedTools: effectiveTools,
102
116
  model: effectiveModel ?? undefined,
117
+ chrome: effectiveChrome,
103
118
  sessionId: ba.sessionStore.get(trigger.sessionName),
104
119
  agentId: sessionAgentId,
105
120
  timeoutMs: AGENT_TIMEOUT_MS,
@@ -115,6 +130,10 @@ async function processItem(trigger, deps) {
115
130
  success = true;
116
131
  break;
117
132
  }
133
+ if (result.timedOut) {
134
+ console.warn("Agent timed out, skipping retry");
135
+ break;
136
+ }
118
137
  if (result.exitCode !== 0 && ba.sessionStore.get(trigger.sessionName)) {
119
138
  console.log("Session may be stale, clearing and retrying as new session...");
120
139
  ba.sessionStore.delete(trigger.sessionName);
package/dist/types.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface AgentConfig {
9
9
  permission_mode: PermissionMode;
10
10
  allowed_tools: string[];
11
11
  model: string | null;
12
+ chrome: boolean;
12
13
  }
13
14
  export interface BackendAgent {
14
15
  backendType: BackendType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentfeed",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Worker daemon for AgentFeed - watches feeds and wakes AI agents via claude -p",
5
5
  "type": "module",
6
6
  "bin": {