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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import * as readline from "node:readline";
|
|
3
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
4
|
+
import { existsSync, copyFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { createBackend } from "./backends/index.js";
|
|
7
|
+
const ALL_BACKEND_TYPES = ["claude", "codex", "gemini"];
|
|
8
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
9
|
+
export function getRequiredEnv(name) {
|
|
10
|
+
const value = process.env[name];
|
|
11
|
+
if (!value) {
|
|
12
|
+
console.error(`Required environment variable: ${name}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
export function parsePermissionMode() {
|
|
18
|
+
const idx = process.argv.indexOf("--permission");
|
|
19
|
+
if (idx === -1)
|
|
20
|
+
return "safe";
|
|
21
|
+
const value = process.argv[idx + 1];
|
|
22
|
+
if (value === "yolo")
|
|
23
|
+
return "yolo";
|
|
24
|
+
if (value === "safe")
|
|
25
|
+
return "safe";
|
|
26
|
+
console.error(`Unknown permission mode: "${value}". Use "safe" (default) or "yolo".`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
export function parseAllowedTools() {
|
|
30
|
+
const tools = [];
|
|
31
|
+
for (let i = 0; i < process.argv.length; i++) {
|
|
32
|
+
if (process.argv[i] === "--allowed-tools") {
|
|
33
|
+
// Collect all following args until the next flag (starts with --)
|
|
34
|
+
for (let j = i + 1; j < process.argv.length; j++) {
|
|
35
|
+
if (process.argv[j].startsWith("--"))
|
|
36
|
+
break;
|
|
37
|
+
tools.push(process.argv[j]);
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return tools;
|
|
43
|
+
}
|
|
44
|
+
export function detectInstalledBackends() {
|
|
45
|
+
return ALL_BACKEND_TYPES.filter((type) => {
|
|
46
|
+
const backend = createBackend(type);
|
|
47
|
+
try {
|
|
48
|
+
execFileSync("which", [backend.binaryName], { stdio: "ignore" });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function probeBackend(type) {
|
|
57
|
+
const backend = createBackend(type);
|
|
58
|
+
// Minimal args to trigger auth check without heavy work
|
|
59
|
+
let args;
|
|
60
|
+
switch (type) {
|
|
61
|
+
case "claude":
|
|
62
|
+
args = ["-p", "say ok", "--output-format", "stream-json", "--max-turns", "1"];
|
|
63
|
+
break;
|
|
64
|
+
case "gemini":
|
|
65
|
+
args = ["say ok", "--output-format", "stream-json"];
|
|
66
|
+
break;
|
|
67
|
+
case "codex":
|
|
68
|
+
args = ["exec", "--json", "--skip-git-repo-check", "--full-auto", "say ok"];
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
const env = backend.buildEnv({
|
|
72
|
+
PATH: process.env.PATH ?? "",
|
|
73
|
+
HOME: process.env.HOME ?? "",
|
|
74
|
+
USER: process.env.USER ?? "",
|
|
75
|
+
SHELL: process.env.SHELL ?? "/bin/sh",
|
|
76
|
+
LANG: process.env.LANG ?? "en_US.UTF-8",
|
|
77
|
+
TERM: process.env.TERM ?? "xterm-256color",
|
|
78
|
+
});
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
try {
|
|
81
|
+
const proc = spawn(backend.binaryName, args, { env, stdio: "pipe" });
|
|
82
|
+
const timer = setTimeout(() => {
|
|
83
|
+
// Still alive after timeout = authenticated (API call in progress)
|
|
84
|
+
proc.kill("SIGTERM");
|
|
85
|
+
resolve(true);
|
|
86
|
+
}, PROBE_TIMEOUT_MS);
|
|
87
|
+
proc.on("error", () => {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
resolve(false);
|
|
90
|
+
});
|
|
91
|
+
proc.on("close", (code) => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
// Quick exit with 0 = completed ok, non-zero = auth/config failure
|
|
94
|
+
resolve(code === 0);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
resolve(false);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export function confirmYolo() {
|
|
103
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
104
|
+
return new Promise((resolve) => {
|
|
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
|
+
rl.question(" Continue? (y/N): ", (answer) => {
|
|
111
|
+
rl.close();
|
|
112
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
export function migrateSessionFile(backendType) {
|
|
117
|
+
const dir = path.join(homedir(), ".agentfeed");
|
|
118
|
+
const legacyPath = path.join(dir, "sessions.json");
|
|
119
|
+
const newPath = path.join(dir, `sessions-${backendType}.json`);
|
|
120
|
+
if (!existsSync(newPath) && existsSync(legacyPath)) {
|
|
121
|
+
try {
|
|
122
|
+
copyFileSync(legacyPath, newPath);
|
|
123
|
+
console.log(`Migrated sessions.json → sessions-${backendType}.json`);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
console.warn(`Failed to migrate session file:`, err);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
package/dist/follow-store.d.ts
CHANGED
package/dist/follow-store.js
CHANGED
|
@@ -17,10 +17,22 @@ export class FollowStore extends PersistentStore {
|
|
|
17
17
|
has(postId) {
|
|
18
18
|
return this.posts.has(postId);
|
|
19
19
|
}
|
|
20
|
+
static MAX_SIZE = 500;
|
|
20
21
|
add(postId) {
|
|
21
22
|
if (this.posts.has(postId))
|
|
22
23
|
return;
|
|
23
24
|
this.posts.add(postId);
|
|
25
|
+
// Evict oldest entries if over limit
|
|
26
|
+
if (this.posts.size > FollowStore.MAX_SIZE) {
|
|
27
|
+
const iter = this.posts.values();
|
|
28
|
+
while (this.posts.size > FollowStore.MAX_SIZE) {
|
|
29
|
+
const oldest = iter.next().value;
|
|
30
|
+
if (oldest !== undefined)
|
|
31
|
+
this.posts.delete(oldest);
|
|
32
|
+
else
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
24
36
|
this.save();
|
|
25
37
|
}
|
|
26
38
|
getAll() {
|
package/dist/index.js
CHANGED
|
@@ -1,74 +1,31 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { AgentFeedClient } from "./api-client.js";
|
|
3
4
|
import { connectSSE } from "./sse-client.js";
|
|
4
|
-
import {
|
|
5
|
-
import { invokeAgent } from "./invoker.js";
|
|
5
|
+
import { detectTriggers } from "./trigger.js";
|
|
6
6
|
import { scanUnprocessed } from "./scanner.js";
|
|
7
7
|
import { SessionStore } from "./session-store.js";
|
|
8
8
|
import { FollowStore } from "./follow-store.js";
|
|
9
9
|
import { QueueStore } from "./queue-store.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
console.error(`Required environment variable: ${name}`);
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
function parsePermissionMode() {
|
|
21
|
-
const idx = process.argv.indexOf("--permission");
|
|
22
|
-
if (idx === -1)
|
|
23
|
-
return "safe";
|
|
24
|
-
const value = process.argv[idx + 1];
|
|
25
|
-
if (value === "yolo")
|
|
26
|
-
return "yolo";
|
|
27
|
-
if (value === "safe")
|
|
28
|
-
return "safe";
|
|
29
|
-
console.error(`Unknown permission mode: "${value}". Use "safe" (default) or "yolo".`);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
function parseAllowedTools() {
|
|
33
|
-
const tools = [];
|
|
34
|
-
for (let i = 0; i < process.argv.length; i++) {
|
|
35
|
-
if (process.argv[i] === "--allowed-tools") {
|
|
36
|
-
// Collect all following args until the next flag (starts with --)
|
|
37
|
-
for (let j = i + 1; j < process.argv.length; j++) {
|
|
38
|
-
if (process.argv[j].startsWith("--"))
|
|
39
|
-
break;
|
|
40
|
-
tools.push(process.argv[j]);
|
|
41
|
-
}
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return tools;
|
|
46
|
-
}
|
|
47
|
-
function confirmYolo() {
|
|
48
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
49
|
-
return new Promise((resolve) => {
|
|
50
|
-
console.log("");
|
|
51
|
-
console.log(" \x1b[33m⚠️ YOLO mode enabled. The agent can do literally anything.\x1b[0m");
|
|
52
|
-
console.log(" \x1b[33m No prompt sandboxing. No trust boundaries.\x1b[0m");
|
|
53
|
-
console.log(" \x1b[33m Prompt injection? Not your problem today.\x1b[0m");
|
|
54
|
-
console.log("");
|
|
55
|
-
rl.question(" Continue? (y/N): ", (answer) => {
|
|
56
|
-
rl.close();
|
|
57
|
-
resolve(answer.trim().toLowerCase() === "y");
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
}
|
|
10
|
+
import { PostSessionStore } from "./post-session-store.js";
|
|
11
|
+
import { AgentRegistryStore } from "./agent-registry-store.js";
|
|
12
|
+
import { createBackend } from "./backends/index.js";
|
|
13
|
+
import { handleTriggers } from "./processor.js";
|
|
14
|
+
import { getRequiredEnv, parsePermissionMode, parseAllowedTools, detectInstalledBackends, probeBackend, confirmYolo, migrateSessionFile, } from "./cli.js";
|
|
61
15
|
const serverUrl = getRequiredEnv("AGENTFEED_URL");
|
|
62
16
|
const apiKey = getRequiredEnv("AGENTFEED_API_KEY");
|
|
63
17
|
const permissionMode = parsePermissionMode();
|
|
64
18
|
const extraAllowedTools = parseAllowedTools();
|
|
19
|
+
const baseName = process.env.AGENTFEED_AGENT_NAME ?? path.basename(process.cwd());
|
|
65
20
|
const client = new AgentFeedClient(serverUrl, apiKey);
|
|
66
|
-
let isRunning = false;
|
|
67
21
|
let sseConnection = null;
|
|
68
|
-
const wakeAttempts = new Map();
|
|
69
|
-
const sessionStore = new SessionStore(process.env.AGENTFEED_SESSION_FILE);
|
|
70
22
|
const followStore = new FollowStore(process.env.AGENTFEED_FOLLOW_FILE);
|
|
71
23
|
const queueStore = new QueueStore(process.env.AGENTFEED_QUEUE_FILE);
|
|
24
|
+
const postSessionStore = new PostSessionStore(process.env.AGENTFEED_POST_SESSION_FILE);
|
|
25
|
+
const agentRegistry = new AgentRegistryStore(process.env.AGENTFEED_AGENT_REGISTRY_FILE);
|
|
26
|
+
// Populated during init
|
|
27
|
+
const backendAgentMap = new Map();
|
|
28
|
+
let backendAgents = [];
|
|
72
29
|
function shutdown() {
|
|
73
30
|
console.log("\nShutting down...");
|
|
74
31
|
sseConnection?.close();
|
|
@@ -76,6 +33,44 @@ function shutdown() {
|
|
|
76
33
|
}
|
|
77
34
|
process.on("SIGINT", shutdown);
|
|
78
35
|
process.on("SIGTERM", shutdown);
|
|
36
|
+
function isNamedSession(sessionName) {
|
|
37
|
+
// Named Sessions are explicitly created via @bot/session-name mentions.
|
|
38
|
+
// PostId-based sessions (ps_xxx) are internal CLI session tracking — not separate agents.
|
|
39
|
+
return sessionName !== "default" && !sessionName.startsWith("ps_");
|
|
40
|
+
}
|
|
41
|
+
async function ensureSessionAgent(sessionName, agentName, backendType) {
|
|
42
|
+
// Only Named Sessions get their own agent identity
|
|
43
|
+
if (!isNamedSession(sessionName)) {
|
|
44
|
+
const ba = backendAgentMap.get(backendType);
|
|
45
|
+
return ba?.agent.id ?? client.agentId;
|
|
46
|
+
}
|
|
47
|
+
const fullName = `${agentName}/${sessionName}`;
|
|
48
|
+
// Check registry first
|
|
49
|
+
const cachedId = agentRegistry.get(fullName);
|
|
50
|
+
if (cachedId)
|
|
51
|
+
return cachedId;
|
|
52
|
+
// Register a new session-agent
|
|
53
|
+
console.log(`Registering session agent: ${fullName}`);
|
|
54
|
+
const sessionAgent = await client.registerAgent(fullName, backendType);
|
|
55
|
+
agentRegistry.set(fullName, sessionAgent.id);
|
|
56
|
+
return sessionAgent.id;
|
|
57
|
+
}
|
|
58
|
+
function getProcessorDeps() {
|
|
59
|
+
return {
|
|
60
|
+
client,
|
|
61
|
+
apiKey,
|
|
62
|
+
serverUrl,
|
|
63
|
+
permissionMode,
|
|
64
|
+
extraAllowedTools,
|
|
65
|
+
backendAgentMap,
|
|
66
|
+
backendAgents,
|
|
67
|
+
followStore,
|
|
68
|
+
queueStore,
|
|
69
|
+
postSessionStore,
|
|
70
|
+
agentRegistry,
|
|
71
|
+
ensureSessionAgent,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
79
74
|
async function main() {
|
|
80
75
|
// Confirm yolo mode
|
|
81
76
|
if (permissionMode === "yolo") {
|
|
@@ -85,36 +80,128 @@ async function main() {
|
|
|
85
80
|
process.exit(0);
|
|
86
81
|
}
|
|
87
82
|
}
|
|
83
|
+
// Auto-detect installed backends and probe auth
|
|
84
|
+
const installedBackends = detectInstalledBackends();
|
|
85
|
+
if (installedBackends.length === 0) {
|
|
86
|
+
console.error("No supported CLI backends found. Install one of: claude, codex, gemini");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
console.log(`Installed backends: ${installedBackends.join(", ")}. Probing auth...`);
|
|
90
|
+
const availableBackends = [];
|
|
91
|
+
for (const type of installedBackends) {
|
|
92
|
+
const ok = await probeBackend(type);
|
|
93
|
+
if (ok) {
|
|
94
|
+
availableBackends.push(type);
|
|
95
|
+
console.log(` ${type}: ✓ authenticated`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.log(` ${type}: ✗ not authenticated, skipping`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (availableBackends.length === 0) {
|
|
102
|
+
console.error("No authenticated backends found. Please log in to at least one CLI.");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
88
105
|
const toolsInfo = extraAllowedTools.length > 0
|
|
89
106
|
? ` + ${extraAllowedTools.join(", ")}`
|
|
90
107
|
: "";
|
|
91
|
-
console.log(`AgentFeed Worker starting... (permission: ${permissionMode}${toolsInfo})`);
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
console.log(`AgentFeed Worker starting... (backends: ${availableBackends.join(", ")}, permission: ${permissionMode}${toolsInfo})`);
|
|
109
|
+
// Register an agent for each available backend
|
|
110
|
+
for (const type of availableBackends) {
|
|
111
|
+
// Migrate legacy session file for first backend (usually claude)
|
|
112
|
+
migrateSessionFile(type);
|
|
113
|
+
const backend = createBackend(type);
|
|
114
|
+
const agentName = `${baseName}/${type}`;
|
|
115
|
+
const agent = await client.registerAgent(agentName, type);
|
|
116
|
+
if (!client.agentId)
|
|
117
|
+
client.setDefaultAgentId(agent.id);
|
|
118
|
+
const sessionStore = new SessionStore(path.join(homedir(), ".agentfeed", `sessions-${type}.json`));
|
|
119
|
+
const ba = { backendType: type, backend, agent, sessionStore };
|
|
120
|
+
// Fetch per-agent config from server (permission_mode, allowed_tools)
|
|
121
|
+
try {
|
|
122
|
+
const config = await client.getAgentConfig(agent.id);
|
|
123
|
+
ba.config = config;
|
|
124
|
+
console.log(`Agent: ${agent.name} (${agent.id}) [${type}] (server config: ${config.permission_mode}, tools: ${config.allowed_tools.length})`);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
console.log(`Agent: ${agent.name} (${agent.id}) [${type}] (using CLI defaults)`);
|
|
128
|
+
}
|
|
129
|
+
backendAgentMap.set(type, ba);
|
|
130
|
+
agentRegistry.set(agentName, agent.id);
|
|
131
|
+
}
|
|
132
|
+
backendAgents = Array.from(backendAgentMap.values());
|
|
133
|
+
// Confirm yolo if any server config overrides to yolo (and CLI didn't already confirm)
|
|
134
|
+
if (permissionMode !== "yolo") {
|
|
135
|
+
const hasServerYolo = backendAgents.some((ba) => ba.config?.permission_mode === "yolo");
|
|
136
|
+
if (hasServerYolo) {
|
|
137
|
+
console.log("\nServer config has yolo permission for one or more agents.");
|
|
138
|
+
const confirmed = await confirmYolo();
|
|
139
|
+
if (!confirmed) {
|
|
140
|
+
console.log("Cancelled. Update agent permissions on the server to use safe mode.");
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Register Named Session agents for all backends
|
|
146
|
+
for (const ba of backendAgents) {
|
|
147
|
+
const namedSessions = ba.sessionStore.keys().filter(isNamedSession);
|
|
148
|
+
for (const sessionName of namedSessions) {
|
|
149
|
+
try {
|
|
150
|
+
await ensureSessionAgent(sessionName, ba.agent.name, ba.backendType);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
console.warn(`Failed to register session agent ${ba.agent.name}/${sessionName}:`, err);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (namedSessions.length > 0) {
|
|
157
|
+
console.log(`Registered ${namedSessions.length} named session agent(s) for ${ba.backendType}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const deps = getProcessorDeps();
|
|
97
161
|
// Step 1: Startup scan for unprocessed items
|
|
98
162
|
console.log("Scanning for unprocessed items...");
|
|
99
|
-
const
|
|
163
|
+
const ownAgentIds = agentRegistry.getAllIds();
|
|
164
|
+
const unprocessed = await scanUnprocessed(client, backendAgents, followStore, postSessionStore, ownAgentIds);
|
|
100
165
|
if (unprocessed.length > 0) {
|
|
101
166
|
console.log(`Found ${unprocessed.length} unprocessed item(s)`);
|
|
102
|
-
|
|
167
|
+
handleTriggers(unprocessed, deps);
|
|
103
168
|
}
|
|
104
169
|
else {
|
|
105
170
|
console.log("No unprocessed items found.");
|
|
106
171
|
}
|
|
107
172
|
// Step 2: Connect to global SSE stream
|
|
108
|
-
const sseUrl = `${serverUrl}/api/events/stream
|
|
173
|
+
const sseUrl = `${serverUrl}/api/events/stream`;
|
|
109
174
|
console.log("Connecting to global event stream...");
|
|
110
|
-
|
|
175
|
+
// Collect all backend agent IDs for online tracking via single SSE connection
|
|
176
|
+
const allAgentIds = backendAgents.map((ba) => ba.agent.id);
|
|
177
|
+
sseConnection = connectSSE(sseUrl, apiKey, allAgentIds, (rawEvent) => {
|
|
111
178
|
if (rawEvent.type === "heartbeat")
|
|
112
179
|
return;
|
|
180
|
+
// Handle session_deleted events directly
|
|
181
|
+
if (rawEvent.type === "session_deleted") {
|
|
182
|
+
try {
|
|
183
|
+
const data = JSON.parse(rawEvent.data);
|
|
184
|
+
const allIds = agentRegistry.getAllIds();
|
|
185
|
+
if (allIds.has(data.agent_id)) {
|
|
186
|
+
// Delete from all backend session stores
|
|
187
|
+
for (const ba of backendAgents) {
|
|
188
|
+
ba.sessionStore.delete(data.session_name);
|
|
189
|
+
}
|
|
190
|
+
postSessionStore.removeBySessionName(data.session_name);
|
|
191
|
+
console.log(`Session deleted: ${data.session_name}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
console.error("Failed to handle session_deleted event:", err);
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
113
199
|
try {
|
|
114
200
|
const event = JSON.parse(rawEvent.data);
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
201
|
+
const currentOwnIds = agentRegistry.getAllIds();
|
|
202
|
+
const triggers = detectTriggers(event, backendAgents, followStore, postSessionStore, currentOwnIds);
|
|
203
|
+
if (triggers.length > 0) {
|
|
204
|
+
handleTriggers(triggers, deps);
|
|
118
205
|
}
|
|
119
206
|
}
|
|
120
207
|
catch (err) {
|
|
@@ -125,130 +212,6 @@ async function main() {
|
|
|
125
212
|
});
|
|
126
213
|
console.log("Worker ready. Listening for events...");
|
|
127
214
|
}
|
|
128
|
-
async function handleTriggers(triggers, agent, skillMd) {
|
|
129
|
-
// Queue all incoming triggers (persisted to disk)
|
|
130
|
-
for (const t of triggers) {
|
|
131
|
-
queueStore.push(t);
|
|
132
|
-
console.log(`Queued trigger: ${t.triggerType} on ${t.postId} (queue size: ${queueStore.size})`);
|
|
133
|
-
}
|
|
134
|
-
if (isRunning)
|
|
135
|
-
return;
|
|
136
|
-
// Process queue until empty
|
|
137
|
-
await processQueue(agent, skillMd);
|
|
138
|
-
}
|
|
139
|
-
async function processQueue(agent, skillMd) {
|
|
140
|
-
while (true) {
|
|
141
|
-
const queued = queueStore.drain();
|
|
142
|
-
if (queued.length === 0)
|
|
143
|
-
break;
|
|
144
|
-
// Filter by wake attempt limit
|
|
145
|
-
const eligible = queued.filter((t) => {
|
|
146
|
-
const attempts = wakeAttempts.get(t.eventId) ?? 0;
|
|
147
|
-
if (attempts >= MAX_WAKE_ATTEMPTS) {
|
|
148
|
-
console.log(`Skipping ${t.eventId}: max wake attempts reached`);
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
return true;
|
|
152
|
-
});
|
|
153
|
-
if (eligible.length === 0)
|
|
154
|
-
break;
|
|
155
|
-
isRunning = true;
|
|
156
|
-
const trigger = eligible[0];
|
|
157
|
-
// Re-queue remaining items
|
|
158
|
-
for (const t of eligible.slice(1)) {
|
|
159
|
-
queueStore.push(t);
|
|
160
|
-
}
|
|
161
|
-
wakeAttempts.set(trigger.eventId, (wakeAttempts.get(trigger.eventId) ?? 0) + 1);
|
|
162
|
-
// Auto-follow thread on mention (so future comments trigger without re-mention)
|
|
163
|
-
if (trigger.triggerType === "mention") {
|
|
164
|
-
followStore.add(trigger.postId);
|
|
165
|
-
console.log(`Following thread: ${trigger.postId}`);
|
|
166
|
-
}
|
|
167
|
-
// Fetch recent context for the prompt
|
|
168
|
-
const recentContext = await fetchContext(trigger);
|
|
169
|
-
console.log(`Waking agent for: ${trigger.triggerType} on ${trigger.postId}`);
|
|
170
|
-
// Report thinking status
|
|
171
|
-
await client.setAgentStatus({
|
|
172
|
-
status: "thinking",
|
|
173
|
-
feed_id: trigger.feedId,
|
|
174
|
-
post_id: trigger.postId,
|
|
175
|
-
});
|
|
176
|
-
let retries = 0;
|
|
177
|
-
let success = false;
|
|
178
|
-
try {
|
|
179
|
-
while (retries < MAX_CRASH_RETRIES) {
|
|
180
|
-
try {
|
|
181
|
-
const result = await invokeAgent({
|
|
182
|
-
agent,
|
|
183
|
-
trigger,
|
|
184
|
-
skillMd,
|
|
185
|
-
apiKey,
|
|
186
|
-
serverUrl,
|
|
187
|
-
recentContext,
|
|
188
|
-
permissionMode,
|
|
189
|
-
extraAllowedTools,
|
|
190
|
-
sessionId: sessionStore.get(trigger.postId),
|
|
191
|
-
});
|
|
192
|
-
if (result.sessionId) {
|
|
193
|
-
sessionStore.set(trigger.postId, result.sessionId);
|
|
194
|
-
}
|
|
195
|
-
if (result.exitCode === 0) {
|
|
196
|
-
success = true;
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
// If resume failed (stale session), clear it and retry as new session
|
|
200
|
-
if (result.exitCode !== 0 && sessionStore.get(trigger.postId)) {
|
|
201
|
-
console.log("Session may be stale, clearing and retrying as new session...");
|
|
202
|
-
sessionStore.delete(trigger.postId);
|
|
203
|
-
}
|
|
204
|
-
console.error(`Agent exited with code ${result.exitCode}, retry ${retries + 1}/${MAX_CRASH_RETRIES}`);
|
|
205
|
-
}
|
|
206
|
-
catch (err) {
|
|
207
|
-
console.error("Agent invocation error:", err);
|
|
208
|
-
}
|
|
209
|
-
retries++;
|
|
210
|
-
}
|
|
211
|
-
if (!success) {
|
|
212
|
-
console.error(`Agent failed after ${MAX_CRASH_RETRIES} retries`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
finally {
|
|
216
|
-
// Always report idle when done
|
|
217
|
-
await client.setAgentStatus({
|
|
218
|
-
status: "idle",
|
|
219
|
-
feed_id: trigger.feedId,
|
|
220
|
-
post_id: trigger.postId,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
isRunning = false;
|
|
224
|
-
// Post-completion: re-scan for items that arrived during execution and add to queue
|
|
225
|
-
try {
|
|
226
|
-
const agent2 = await client.getMe();
|
|
227
|
-
const newUnprocessed = await scanUnprocessed(client, agent2, followStore);
|
|
228
|
-
for (const t of newUnprocessed) {
|
|
229
|
-
queueStore.push(t);
|
|
230
|
-
}
|
|
231
|
-
if (newUnprocessed.length > 0) {
|
|
232
|
-
console.log(`Post-completion scan: ${newUnprocessed.length} item(s) added to queue`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
catch (err) {
|
|
236
|
-
console.error("Post-completion scan error:", err);
|
|
237
|
-
}
|
|
238
|
-
// Loop continues to process next item in queue
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
async function fetchContext(trigger) {
|
|
242
|
-
try {
|
|
243
|
-
const comments = await client.getPostComments(trigger.postId, { limit: 10 });
|
|
244
|
-
return comments.data
|
|
245
|
-
.map((c) => `[${c.author_type}${c.author_name ? ` (${c.author_name})` : ""}] ${c.content}`)
|
|
246
|
-
.join("\n");
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
return "";
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
215
|
main().catch((err) => {
|
|
253
216
|
console.error("Fatal error:", err);
|
|
254
217
|
process.exit(1);
|
package/dist/invoker.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import type { TriggerContext, AgentInfo, PermissionMode } from "./types.js";
|
|
2
|
-
|
|
2
|
+
import type { CLIBackend } from "./backends/index.js";
|
|
3
|
+
export interface InvokeOptions {
|
|
3
4
|
agent: AgentInfo;
|
|
4
5
|
trigger: TriggerContext;
|
|
5
|
-
skillMd: string;
|
|
6
6
|
apiKey: string;
|
|
7
7
|
serverUrl: string;
|
|
8
8
|
recentContext: string;
|
|
9
9
|
permissionMode: PermissionMode;
|
|
10
10
|
extraAllowedTools?: string[];
|
|
11
11
|
sessionId?: string;
|
|
12
|
+
agentId?: string;
|
|
13
|
+
timeoutMs?: number;
|
|
12
14
|
}
|
|
13
|
-
interface InvokeResult {
|
|
15
|
+
export interface InvokeResult {
|
|
14
16
|
exitCode: number;
|
|
15
17
|
sessionId?: string;
|
|
16
18
|
}
|
|
17
|
-
export declare function invokeAgent(options: InvokeOptions): Promise<InvokeResult>;
|
|
18
|
-
export {};
|
|
19
|
+
export declare function invokeAgent(backend: CLIBackend, options: InvokeOptions): Promise<InvokeResult>;
|