agentfeed 0.1.10 → 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,17 +24,26 @@ 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 } = 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,
31
31
  "--mcp-config", this.mcpConfigPath,
32
32
  ];
33
+ if (model) {
34
+ args.push("--model", model);
35
+ }
36
+ if (chrome) {
37
+ args.push("--chrome");
38
+ }
33
39
  if (permissionMode === "yolo") {
34
40
  args.push("--dangerously-skip-permissions");
35
41
  }
36
42
  else {
37
43
  const allowedTools = ["mcp__agentfeed__*", ...(extraAllowedTools ?? [])];
44
+ if (chrome) {
45
+ allowedTools.push("mcp__claude-in-chrome__*");
46
+ }
38
47
  for (const tool of allowedTools) {
39
48
  args.push("--allowedTools", tool);
40
49
  }
@@ -10,8 +10,11 @@ export class CodexBackend {
10
10
  this.mcpEnv = env;
11
11
  }
12
12
  buildArgs(options) {
13
- const { prompt, systemPrompt, sessionId, permissionMode } = options;
13
+ const { prompt, systemPrompt, sessionId, permissionMode, model } = options;
14
14
  const args = ["exec"];
15
+ if (model) {
16
+ args.push("-m", model);
17
+ }
15
18
  // MCP config via dot-notation -c flags (codex-cli 0.46+ requires struct, not JSON string)
16
19
  const prefix = "mcp_servers.agentfeed";
17
20
  args.push("-c", `${prefix}.command=${this.mcpCommand}`);
@@ -33,10 +33,13 @@ export class GeminiBackend {
33
33
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
34
34
  }
35
35
  buildArgs(options) {
36
- const { prompt, systemPrompt, sessionId, permissionMode, extraAllowedTools } = options;
36
+ const { prompt, systemPrompt, sessionId, permissionMode, extraAllowedTools, model } = options;
37
37
  // Gemini has no --append-system-prompt flag, embed in user prompt
38
38
  const fullPrompt = `[System Instructions]\n${systemPrompt}\n\n[Task]\n${prompt}`;
39
39
  const args = [fullPrompt];
40
+ if (model) {
41
+ args.push("--model", model);
42
+ }
40
43
  if (sessionId) {
41
44
  args.push("--resume", sessionId);
42
45
  }
@@ -6,6 +6,8 @@ export interface BuildArgsOptions {
6
6
  sessionId?: string;
7
7
  permissionMode: PermissionMode;
8
8
  extraAllowedTools?: string[];
9
+ model?: string;
10
+ chrome?: boolean;
9
11
  }
10
12
  export interface CLIBackend {
11
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/index.js CHANGED
@@ -121,7 +121,8 @@ async function main() {
121
121
  try {
122
122
  const config = await client.getAgentConfig(agent.id);
123
123
  ba.config = config;
124
- console.log(`Agent: ${agent.name} (${agent.id}) [${type}] (server config: ${config.permission_mode}, tools: ${config.allowed_tools.length})`);
124
+ const modelLabel = config.model ? `, model: ${config.model}` : "";
125
+ console.log(`Agent: ${agent.name} (${agent.id}) [${type}] (server config: ${config.permission_mode}, tools: ${config.allowed_tools.length}${modelLabel})`);
125
126
  }
126
127
  catch {
127
128
  console.log(`Agent: ${agent.name} (${agent.id}) [${type}] (using CLI defaults)`);
package/dist/invoker.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface InvokeOptions {
8
8
  recentContext: string;
9
9
  permissionMode: PermissionMode;
10
10
  extraAllowedTools?: string[];
11
+ model?: string;
12
+ chrome?: boolean;
11
13
  sessionId?: string;
12
14
  agentId?: string;
13
15
  timeoutMs?: number;
@@ -15,5 +17,6 @@ export interface InvokeOptions {
15
17
  export interface InvokeResult {
16
18
  exitCode: number;
17
19
  sessionId?: string;
20
+ timedOut: boolean;
18
21
  }
19
22
  export declare function invokeAgent(backend: CLIBackend, options: InvokeOptions): Promise<InvokeResult>;
package/dist/invoker.js CHANGED
@@ -41,6 +41,8 @@ export function invokeAgent(backend, options) {
41
41
  sessionId: options.sessionId,
42
42
  permissionMode: options.permissionMode,
43
43
  extraAllowedTools: options.extraAllowedTools,
44
+ model: options.model,
45
+ chrome: options.chrome,
44
46
  });
45
47
  const env = backend.buildEnv({
46
48
  ...agentfeedEnv,
@@ -54,12 +56,14 @@ export function invokeAgent(backend, options) {
54
56
  console.log(`Invoking ${backend.name}...`);
55
57
  const child = spawn(backend.binaryName, args, {
56
58
  env,
57
- stdio: isNewSession ? ["inherit", "pipe", "inherit"] : "inherit",
59
+ stdio: isNewSession ? ["ignore", "pipe", "inherit"] : ["ignore", "inherit", "inherit"],
58
60
  });
59
61
  // Timeout watchdog
60
62
  let killTimer = null;
63
+ let timedOut = false;
61
64
  if (options.timeoutMs) {
62
65
  killTimer = setTimeout(() => {
66
+ timedOut = true;
63
67
  console.warn(`Agent timed out after ${options.timeoutMs / 1000}s, killing process...`);
64
68
  child.kill("SIGTERM");
65
69
  setTimeout(() => { if (!child.killed)
@@ -103,7 +107,7 @@ export function invokeAgent(backend, options) {
103
107
  if (isNewSession)
104
108
  process.stdout.write("\n");
105
109
  console.log(`Agent exited (code ${code ?? "unknown"})`);
106
- resolve({ exitCode: code ?? 1, sessionId: sessionId ?? options.sessionId });
110
+ resolve({ exitCode: code ?? 1, sessionId: sessionId ?? options.sessionId, timedOut });
107
111
  });
108
112
  });
109
113
  }
@@ -137,7 +141,15 @@ function buildSystemPrompt(options) {
137
141
  - agentfeed_create_post - Create a new post in a feed
138
142
  - agentfeed_get_comments - Get comments on a post (use since/author_type filters)
139
143
  - agentfeed_post_comment - Post a comment (Korean and emoji supported!)
140
- - 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
+ : "";
141
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.`;
142
154
  if (options.permissionMode === "yolo") {
143
155
  return `# AgentFeed
@@ -148,7 +160,7 @@ ${toolList}
148
160
 
149
161
  Use these tools to interact with the feed. All content encoding is handled automatically.
150
162
 
151
- ${imageGuidance}`;
163
+ ${imageGuidance}${chromeSection}`;
152
164
  }
153
165
  return `${SECURITY_POLICY}
154
166
 
@@ -160,7 +172,7 @@ ${toolList}
160
172
 
161
173
  Use these tools to interact with the feed. All content encoding is handled automatically.
162
174
 
163
- ${imageGuidance}`;
175
+ ${imageGuidance}${chromeSection}`;
164
176
  }
165
177
  function wrapUntrusted(text) {
166
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);
@@ -85,6 +98,8 @@ async function processItem(trigger, deps) {
85
98
  const effectiveTools = ba.config?.allowed_tools?.length
86
99
  ? ba.config.allowed_tools
87
100
  : deps.extraAllowedTools;
101
+ const effectiveModel = ba.config?.model;
102
+ const effectiveChrome = ba.config?.chrome ?? false;
88
103
  let retries = 0;
89
104
  let success = false;
90
105
  try {
@@ -98,6 +113,8 @@ async function processItem(trigger, deps) {
98
113
  recentContext,
99
114
  permissionMode: effectivePermission,
100
115
  extraAllowedTools: effectiveTools,
116
+ model: effectiveModel ?? undefined,
117
+ chrome: effectiveChrome,
101
118
  sessionId: ba.sessionStore.get(trigger.sessionName),
102
119
  agentId: sessionAgentId,
103
120
  timeoutMs: AGENT_TIMEOUT_MS,
@@ -113,6 +130,10 @@ async function processItem(trigger, deps) {
113
130
  success = true;
114
131
  break;
115
132
  }
133
+ if (result.timedOut) {
134
+ console.warn("Agent timed out, skipping retry");
135
+ break;
136
+ }
116
137
  if (result.exitCode !== 0 && ba.sessionStore.get(trigger.sessionName)) {
117
138
  console.log("Session may be stale, clearing and retrying as new session...");
118
139
  ba.sessionStore.delete(trigger.sessionName);
package/dist/types.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface AgentInfo {
8
8
  export interface AgentConfig {
9
9
  permission_mode: PermissionMode;
10
10
  allowed_tools: string[];
11
+ model: string | null;
12
+ chrome: boolean;
11
13
  }
12
14
  export interface BackendAgent {
13
15
  backendType: BackendType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentfeed",
3
- "version": "0.1.10",
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": {