agorai 0.4.4 → 0.6.0

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/README.md CHANGED
@@ -75,12 +75,16 @@ Your PC / VPS
75
75
  │ │
76
76
  │ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
77
77
  │ │ Projects │ │ Convos │ │ Shared Memory │ │
78
- │ │ │ │ @mentions │ │ per-project │ │
78
+ │ │ + Tasks │ │ + Whisper │ │ + Agent Memory │ │
79
79
  │ └──────────┘ └───────────┘ └──────────────────┘ │
80
80
  │ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
81
81
  │ │ Auth │ │ Rate │ │ 4-level │ │
82
82
  │ │ (salted) │ │ limiting │ │ visibility │ │
83
83
  │ └──────────┘ └───────────┘ └──────────────────┘ │
84
+ │ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
85
+ │ │ Capabil. │ │ Skills │ │ 35 MCP tools │ │
86
+ │ │ catalog │ │ system │ │ + SSE push │ │
87
+ │ └──────────┘ └───────────┘ └──────────────────┘ │
84
88
  │ SQLite │
85
89
  └────────────────────┬─────────────────────────────┘
86
90
  │ HTTP (MCP protocol)
@@ -94,7 +98,7 @@ Your PC / VPS
94
98
 
95
99
  Two npm packages:
96
100
 
97
- - **`agorai`** — The bridge server. Hosts projects, conversations, shared memory, auth, and 16 MCP tools over HTTP. SQLite storage, zero external services. Can also run internal agents in the same process via `--with-agent`.
101
+ - **`agorai`** — The bridge server. Hosts projects, conversations, shared memory, auth, and 35 MCP tools over HTTP. SQLite storage, zero external services. Can also run internal agents in the same process via `--with-agent`.
98
102
  - **`agorai-connect`** — Connects any agent to the bridge. MCP proxy for Claude Desktop, interactive setup wizard, and an agent runner for OpenAI-compatible models.
99
103
 
100
104
  ## Key features
@@ -120,6 +124,18 @@ Two npm packages:
120
124
  npx agorai debate "Redis vs Memcached for session storage?"
121
125
  ```
122
126
 
127
+ **Task claiming** — Create tasks with required capabilities, claim them atomically (no race conditions), complete with results. Stale claims auto-release when agents go offline. Pull model — agents discover and claim work, not push.
128
+
129
+ **Directed messages (whisper)** — Send private messages to specific agents with `recipients`. Only listed agents and the sender can see the message. Store-enforced — non-recipients never know the message exists. Additive to visibility: both filters apply.
130
+
131
+ **Capability discovery** — Agents register capabilities on connect. `discover_capabilities` lets agents find each other by skill (`code-review`, `analysis`, `code-execution`). The foundation for intelligent task routing.
132
+
133
+ **Skills system** — Progressive disclosure skills replace instructions. Skills have title, summary, instructions hint, full content, and supporting files. Agents receive only metadata (tier 1) on subscribe — load full content (tier 2) and files (tier 3) on demand. Target skills to specific agents by name or by type/capability selector. Tags for filtering. ~80-90% context savings.
134
+
135
+ **Agent memory** — Private per-agent scratchpad with 3 scopes: global, per-project, and per-conversation. Each agent manages its own memory — invisible to other agents. Conversation memory auto-cleans on unsubscribe.
136
+
137
+ **Message tags** — Tag messages with metadata (`review`, `urgent`, `decision`) and filter by tags or sender in `get_messages`. Structured message types (`proposal`, `decision`) enable formal conversation protocols.
138
+
123
139
  **Structured metadata** — Every message carries trusted `bridgeMetadata` (visibility, capping info, confidentiality instructions) and private `agentMetadata` (only visible to the sender). Agents can't forge bridge data.
124
140
 
125
141
  **Security** — Salted HMAC-SHA-256 API key hashing, per-agent rate limiting, input size limits on all fields, visibility-capped writes. Everything localhost by default.
@@ -140,15 +156,16 @@ docker run -v ./agorai.config.json:/app/agorai.config.json -p 3100:3100 agorai/b
140
156
 
141
157
  | Version | Focus |
142
158
  |---------|-------|
143
- | **v0.2** | **Bridge — shared workspace, visibility, auth, 16 MCP tools** |
159
+ | **v0.2** | **Bridge — shared workspace, visibility, auth, MCP tools** |
144
160
  | v0.2.x | Security hardening, Docker, npm publish, session recovery, internal agents |
145
161
  | **v0.3** | **SSE push notifications — real-time message delivery, 3-layer EventBus→Dispatcher→Client** |
146
- | **v0.4** | **Metadata overhaul — bridgeMetadata/agentMetadata, confidentiality modes, high-water marks** |
147
- | v0.4.x | Strict mode enforcement, discovery rules, access control |
148
- | v0.5 | Discover, Decide, Deliver capability catalog, task claiming, structured conversations, directed messages |
149
- | v0.6 | Full-text search, archive conversations, Sentinel AI (auto-classification, redaction) |
150
- | v0.7 | Web dashboard, human participants, A2A protocol support |
151
- | v0.8+ | Enterprise — OAuth/JWT, RBAC, audit trail |
162
+ | **v0.4** | **Metadata overhaul — bridgeMetadata/agentMetadata, confidentiality modes, access requests** |
163
+ | **v0.5** | **Discover, Decide, Deliver — 32 tools: capability catalog, task claiming, whispers, message tags, agent memory, instruction matrix, structured protocol** |
164
+ | **v0.6** | **Skills systemprogressive disclosure (3-tier), agent targeting, skill files, replaces instruction matrix. 35 tools** |
165
+ | v0.7 | Task dependencies, explicit project access control, full-text search, conversation templates |
166
+ | v0.8 | Orchestrator agent, Sentinel AI, debate engine via bridge |
167
+ | v0.9 | Web dashboard, human participants, A2A protocol support |
168
+ | v1.0+ | Enterprise — OAuth/JWT, RBAC, audit trail, SaaS |
152
169
 
153
170
  ## Positioning
154
171
 
@@ -160,6 +177,9 @@ Agorai is **not** another agent framework. It's infrastructure — the collabora
160
177
  | Protocol | MCP (open standard) | Custom | Custom | Custom |
161
178
  | Models | Any (BYOM) | OpenAI-focused | OpenAI-focused | LangChain |
162
179
  | Visibility | 4-level, store-enforced | None | None | None |
180
+ | Task claiming | Atomic, capability-based | Role assignment | None | DAG nodes |
181
+ | Agent memory | Private per-agent, 3 scopes | Shared only | Shared only | None |
182
+ | Directed messages | Whisper (recipients) | None | None | None |
163
183
  | Debate/consensus | Built-in | None | Basic | None |
164
184
  | Local-first | Yes | Cloud-centric | Cloud-centric | Cloud-centric |
165
185
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Bridge HTTP server — Streamable HTTP transport for the MCP bridge.
3
3
  *
4
- * Exposes 16 bridge tools + debate tools over HTTP.
4
+ * Exposes 35 bridge tools + debate tools over HTTP.
5
5
  * Auth is handled via API key in Authorization header.
6
6
  * Each request is authenticated before being passed to the MCP handler.
7
7
  */
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/bridge/server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,WAAW,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA2F3C,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAgeD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC;IAC1E,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC,CAmLD"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/bridge/server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,WAAW,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AA2G3C,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AA+6BD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC;IAC1E,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC,CAqLD"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Bridge HTTP server — Streamable HTTP transport for the MCP bridge.
3
3
  *
4
- * Exposes 16 bridge tools + debate tools over HTTP.
4
+ * Exposes 35 bridge tools + debate tools over HTTP.
5
5
  * Auth is handled via API key in Authorization header.
6
6
  * Each request is authenticated before being passed to the MCP handler.
7
7
  */
@@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
16
16
  import { resolve, dirname } from "node:path";
17
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
18
  const PKG_VERSION = JSON.parse(readFileSync(resolve(__dirname, "../../package.json"), "utf-8")).version;
19
- import { RegisterAgentSchema, ListBridgeAgentsSchema, CreateProjectSchema, ListProjectsSchema, SetMemorySchema, GetMemorySchema, DeleteMemorySchema, CreateConversationSchema, ListConversationsSchema, SubscribeSchema, UnsubscribeSchema, SendMessageSchema, GetMessagesSchema, GetStatusSchema, MarkReadSchema, ListSubscribersSchema, ListAccessRequestsSchema, RespondToAccessRequestSchema, GetMyAccessRequestsSchema, } from "./tools.js";
19
+ import { RegisterAgentSchema, ListBridgeAgentsSchema, DiscoverCapabilitiesSchema, CreateProjectSchema, ListProjectsSchema, SetMemorySchema, GetMemorySchema, DeleteMemorySchema, CreateConversationSchema, ListConversationsSchema, SubscribeSchema, UnsubscribeSchema, SendMessageSchema, GetMessagesSchema, GetStatusSchema, MarkReadSchema, ListSubscribersSchema, ListAccessRequestsSchema, RespondToAccessRequestSchema, GetMyAccessRequestsSchema, CreateTaskSchema, ListTasksSchema, ClaimTaskSchema, CompleteTaskSchema, ReleaseTaskSchema, UpdateTaskSchema, SetSkillSchema, ListSkillsSchema, GetSkillSchema, DeleteSkillSchema, SetSkillFileSchema, GetSkillFileSchema, SetAgentMemorySchema, GetAgentMemorySchema, DeleteAgentMemorySchema, } from "./tools.js";
20
20
  const log = createLogger("bridge");
21
21
  /** Per-session context: maps transport sessionId → agent auth. */
22
22
  const sessionAuth = new Map();
@@ -98,6 +98,13 @@ function createBridgeMcpServer(store, agentId) {
98
98
  "If you try to subscribe to a conversation you don't have access to, an access request is created automatically.",
99
99
  "Subscribers of that conversation can approve or deny your request via list_access_requests + respond_to_access_request.",
100
100
  "Check your request status with get_my_access_requests.",
101
+ "",
102
+ "IMPORTANT — Skills system (progressive disclosure):",
103
+ "Skills provide behavioral instructions and context. They use 3-tier progressive disclosure to save context:",
104
+ "- Tier 1 (metadata): When you subscribe, you receive skill metadata (title, summary, instructions, tags) — NOT the full content.",
105
+ "- Tier 2 (content): Call get_skill(skill_id) to load the full content of a skill you need.",
106
+ "- Tier 3 (files): Call get_skill_file(skill_id, filename) to load supporting files attached to a skill.",
107
+ "Only load tier 2/3 when you actually need the detail. The summary and instructions fields give you enough to decide.",
101
108
  ].join("\n"),
102
109
  });
103
110
  // --- Agent tools ---
@@ -137,6 +144,17 @@ function createBridgeMcpServer(store, agentId) {
137
144
  const safe = filtered.map(({ apiKeyHash: _, ...rest }) => rest);
138
145
  return { content: [{ type: "text", text: JSON.stringify(safe, null, 2) }] };
139
146
  });
147
+ server.tool("discover_capabilities", "Find agents by capability. Without a filter, returns all agents and their capabilities.", DiscoverCapabilitiesSchema.shape, async (args) => {
148
+ let agents;
149
+ if (args.capability) {
150
+ agents = await store.findAgentsByCapability(args.capability);
151
+ }
152
+ else {
153
+ agents = await store.listAgents();
154
+ }
155
+ const safe = agents.map(({ apiKeyHash: _, ...rest }) => rest);
156
+ return { content: [{ type: "text", text: JSON.stringify(safe, null, 2) }] };
157
+ });
140
158
  // --- Project tools ---
141
159
  server.tool("create_project", "Create a new project", CreateProjectSchema.shape, async (args) => {
142
160
  const project = await store.createProject({
@@ -203,9 +221,20 @@ function createBridgeMcpServer(store, agentId) {
203
221
  defaultVisibility: args.default_visibility,
204
222
  createdBy: agentId,
205
223
  });
206
- // Auto-subscribe the creator
224
+ // Auto-subscribe the creator and include skills metadata
207
225
  await store.subscribe(conv.id, agentId);
208
- return { content: [{ type: "text", text: JSON.stringify(conv, null, 2) }] };
226
+ const agent = await store.getAgent(agentId);
227
+ const matchingSkills = agent
228
+ ? await store.getMatchingSkills({ name: agent.name, type: agent.type, capabilities: agent.capabilities }, conv.id)
229
+ : [];
230
+ return { content: [{ type: "text", text: JSON.stringify({
231
+ ...conv,
232
+ skills: matchingSkills.map((s) => ({
233
+ id: s.id, title: s.title, summary: s.summary,
234
+ instructions: s.instructions, tags: s.tags,
235
+ scope: s.scope, agents: s.agents, files: s.files,
236
+ })),
237
+ }, null, 2) }] };
209
238
  });
210
239
  server.tool("list_conversations", "List conversations in a project", ListConversationsSchema.shape, async (args) => {
211
240
  let conversations = await store.listConversations(args.project_id, agentId);
@@ -215,10 +244,23 @@ function createBridgeMcpServer(store, agentId) {
215
244
  return { content: [{ type: "text", text: JSON.stringify(conversations, null, 2) }] };
216
245
  });
217
246
  server.tool("subscribe", "Subscribe to a conversation. If you don't have access, an access request is created automatically — existing subscribers can approve it.", SubscribeSchema.shape, async (args) => {
218
- // Check if already subscribed
247
+ // If already subscribed, return current skills metadata (not an error)
219
248
  const alreadySubscribed = await store.isSubscribed(args.conversation_id, agentId);
220
249
  if (alreadySubscribed) {
221
- return { content: [{ type: "text", text: JSON.stringify({ error: "Already subscribed to this conversation" }) }] };
250
+ const agent = await store.getAgent(agentId);
251
+ const matchingSkills = agent
252
+ ? await store.getMatchingSkills({ name: agent.name, type: agent.type, capabilities: agent.capabilities }, args.conversation_id)
253
+ : [];
254
+ return { content: [{ type: "text", text: JSON.stringify({
255
+ subscribed: true,
256
+ already_subscribed: true,
257
+ conversation_id: args.conversation_id,
258
+ skills: matchingSkills.map((s) => ({
259
+ id: s.id, title: s.title, summary: s.summary,
260
+ instructions: s.instructions, tags: s.tags,
261
+ scope: s.scope, agents: s.agents, files: s.files,
262
+ })),
263
+ }) }] };
222
264
  }
223
265
  // Verify conversation exists and caller can access its project
224
266
  const conv = await store.getConversation(args.conversation_id);
@@ -230,7 +272,25 @@ function createBridgeMcpServer(store, agentId) {
230
272
  await store.subscribe(args.conversation_id, agentId, {
231
273
  historyAccess: args.history_access,
232
274
  });
233
- return { content: [{ type: "text", text: JSON.stringify({ subscribed: true, conversation_id: args.conversation_id }) }] };
275
+ // Include matching skill metadata in subscribe response (progressive disclosure: tier 1 only)
276
+ const agent = await store.getAgent(agentId);
277
+ const matchingSkills = agent
278
+ ? await store.getMatchingSkills({ name: agent.name, type: agent.type, capabilities: agent.capabilities }, args.conversation_id)
279
+ : [];
280
+ return { content: [{ type: "text", text: JSON.stringify({
281
+ subscribed: true,
282
+ conversation_id: args.conversation_id,
283
+ skills: matchingSkills.map((s) => ({
284
+ id: s.id,
285
+ title: s.title,
286
+ summary: s.summary,
287
+ instructions: s.instructions,
288
+ tags: s.tags,
289
+ scope: s.scope,
290
+ agents: s.agents,
291
+ files: s.files,
292
+ })),
293
+ }) }] };
234
294
  }
235
295
  // No project access — fallback to access request
236
296
  // NOTE: Currently triggered by clearance < project visibility. In v0.6, access control
@@ -280,12 +340,30 @@ function createBridgeMcpServer(store, agentId) {
280
340
  const subscribed = await store.isSubscribed(args.conversation_id, agentId);
281
341
  if (!subscribed)
282
342
  return ACCESS_DENIED;
343
+ // Validate @mentions in whispers: mentioned agents must be in recipients list
344
+ if (args.recipients && args.recipients.length > 0) {
345
+ const allAgents = await store.listAgents();
346
+ const agentNameMap = new Map(allAgents.map((a) => [a.name.toLowerCase(), a.id]));
347
+ const mentionPattern = /@([\w-]+)/g;
348
+ let match;
349
+ while ((match = mentionPattern.exec(args.content)) !== null) {
350
+ const mentionedName = match[1].toLowerCase();
351
+ const mentionedId = agentNameMap.get(mentionedName);
352
+ if (mentionedId && mentionedId !== agentId && !args.recipients.includes(mentionedId)) {
353
+ return { content: [{ type: "text", text: JSON.stringify({
354
+ error: `@${match[1]} is mentioned but not in recipients — they won't see this whisper. Add them to recipients or remove the @mention.`,
355
+ }) }] };
356
+ }
357
+ }
358
+ }
283
359
  const message = await store.sendMessage({
284
360
  conversationId: args.conversation_id,
285
361
  fromAgent: agentId,
286
362
  type: args.type,
287
363
  visibility: args.visibility,
288
364
  content: args.content,
365
+ tags: args.tags,
366
+ recipients: args.recipients,
289
367
  metadata: args.metadata,
290
368
  });
291
369
  // Response: include bridgeMetadata + agentMetadata (sender sees their own), exclude deprecated metadata
@@ -301,6 +379,8 @@ function createBridgeMcpServer(store, agentId) {
301
379
  since: args.since,
302
380
  unreadOnly: args.unread_only,
303
381
  limit: args.limit,
382
+ tags: args.tags,
383
+ fromAgent: args.from_agent,
304
384
  });
305
385
  // Shape response: strip agentMetadata for non-sender, exclude deprecated metadata
306
386
  const shaped = messages.map((msg) => {
@@ -312,6 +392,272 @@ function createBridgeMcpServer(store, agentId) {
312
392
  });
313
393
  return { content: [{ type: "text", text: JSON.stringify(shaped, null, 2) }] };
314
394
  });
395
+ // --- Task tools ---
396
+ server.tool("create_task", "Create a task in a project. Other agents can discover and claim it.", CreateTaskSchema.shape, async (args) => {
397
+ const project = await store.getProject(args.project_id, agentId);
398
+ if (!project)
399
+ return ACCESS_DENIED;
400
+ const task = await store.createTask({
401
+ projectId: args.project_id,
402
+ conversationId: args.conversation_id,
403
+ title: args.title,
404
+ description: args.description,
405
+ requiredCapabilities: args.required_capabilities,
406
+ createdBy: agentId,
407
+ });
408
+ return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
409
+ });
410
+ server.tool("list_tasks", "List tasks in a project, optionally filtered by status, capability, or claiming agent", ListTasksSchema.shape, async (args) => {
411
+ const tasks = await store.listTasks(args.project_id, agentId, {
412
+ status: args.status,
413
+ claimedBy: args.claimed_by,
414
+ capability: args.capability,
415
+ });
416
+ return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
417
+ });
418
+ server.tool("claim_task", "Claim an open task. Atomic — only one agent can claim a task at a time.", ClaimTaskSchema.shape, async (args) => {
419
+ const task = await store.claimTask(args.task_id, agentId);
420
+ if (!task) {
421
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Task not available — it may already be claimed or does not exist" }) }] };
422
+ }
423
+ return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
424
+ });
425
+ server.tool("complete_task", "Mark a claimed task as completed with an optional result", CompleteTaskSchema.shape, async (args) => {
426
+ const task = await store.completeTask(args.task_id, agentId, args.result);
427
+ if (!task) {
428
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Cannot complete — task is not claimed by you or does not exist" }) }] };
429
+ }
430
+ return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
431
+ });
432
+ server.tool("release_task", "Release a claimed task back to open so another agent can claim it", ReleaseTaskSchema.shape, async (args) => {
433
+ const task = await store.releaseTask(args.task_id, agentId);
434
+ if (!task) {
435
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Cannot release — task is not claimed or you lack permission" }) }] };
436
+ }
437
+ return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
438
+ });
439
+ server.tool("update_task", "Update a task you created (title, description, or status). Only the creator can update.", UpdateTaskSchema.shape, async (args) => {
440
+ const task = await store.updateTask(args.task_id, agentId, {
441
+ title: args.title,
442
+ description: args.description,
443
+ status: args.status,
444
+ });
445
+ if (!task) {
446
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Cannot update — task not found or you are not the creator" }) }] };
447
+ }
448
+ return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
449
+ });
450
+ // --- Skill tools ---
451
+ server.tool("set_skill", "Create or update a skill in a scope. Creator can always create/edit. Listed agents (in agents[]) can edit existing skills. Use selector and agents[] to target specific agents.", SetSkillSchema.shape, async (args) => {
452
+ let scope = "bridge";
453
+ let scopeId;
454
+ if (args.conversation_id) {
455
+ scope = "conversation";
456
+ scopeId = args.conversation_id;
457
+ const conv = await store.getConversation(args.conversation_id);
458
+ if (!conv)
459
+ return ACCESS_DENIED;
460
+ // Creator can always set. Listed agents can edit existing.
461
+ if (conv.createdBy !== agentId) {
462
+ // Check if caller is a listed agent on an existing skill with this title
463
+ const existing = (await store.listSkills("conversation", args.conversation_id)).find((s) => s.title === args.title);
464
+ if (!existing) {
465
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Only the conversation creator can create new skills" }) }] };
466
+ }
467
+ const agent = await store.getAgent(agentId);
468
+ const agentName = agent?.name ?? "";
469
+ if (!existing.agents.some((a) => a.toLowerCase() === agentName.toLowerCase())) {
470
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Only the creator or listed agents can edit this skill" }) }] };
471
+ }
472
+ }
473
+ }
474
+ else if (args.project_id) {
475
+ scope = "project";
476
+ scopeId = args.project_id;
477
+ const project = await store.getProject(args.project_id, agentId);
478
+ if (!project)
479
+ return ACCESS_DENIED;
480
+ if (project.createdBy !== agentId) {
481
+ const existing = (await store.listSkills("project", args.project_id)).find((s) => s.title === args.title);
482
+ if (!existing) {
483
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Only the project creator can create new skills" }) }] };
484
+ }
485
+ const agent = await store.getAgent(agentId);
486
+ const agentName = agent?.name ?? "";
487
+ if (!existing.agents.some((a) => a.toLowerCase() === agentName.toLowerCase())) {
488
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Only the creator or listed agents can edit this skill" }) }] };
489
+ }
490
+ }
491
+ }
492
+ else {
493
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Bridge-level skills cannot be set via MCP. Provide project_id or conversation_id." }) }] };
494
+ }
495
+ const skill = await store.setSkill({
496
+ scope,
497
+ scopeId,
498
+ title: args.title,
499
+ summary: args.summary,
500
+ instructions: args.instructions,
501
+ selector: args.selector,
502
+ agents: args.agents,
503
+ tags: args.tags,
504
+ content: args.content,
505
+ createdBy: agentId,
506
+ });
507
+ return { content: [{ type: "text", text: JSON.stringify(skill, null, 2) }] };
508
+ });
509
+ server.tool("list_skills", "List skills for a scope (returns metadata only — no content). No params = bridge-level. With project_id = project-level. With conversation_id = conversation-level.", ListSkillsSchema.shape, async (args) => {
510
+ let scope = "bridge";
511
+ let scopeId;
512
+ if (args.conversation_id) {
513
+ scope = "conversation";
514
+ scopeId = args.conversation_id;
515
+ const subscribed = await store.isSubscribed(args.conversation_id, agentId);
516
+ if (!subscribed)
517
+ return ACCESS_DENIED;
518
+ }
519
+ else if (args.project_id) {
520
+ scope = "project";
521
+ scopeId = args.project_id;
522
+ const project = await store.getProject(args.project_id, agentId);
523
+ if (!project)
524
+ return ACCESS_DENIED;
525
+ }
526
+ const skills = await store.listSkills(scope, scopeId, { tags: args.tags });
527
+ // Filter by agent visibility
528
+ const agent = await store.getAgent(agentId);
529
+ const agentName = agent?.name ?? "";
530
+ const visible = skills.filter((s) => {
531
+ if (s.agents.length === 0)
532
+ return true;
533
+ return s.agents.some((a) => a.toLowerCase() === agentName.toLowerCase());
534
+ });
535
+ // Return metadata only (progressive disclosure tier 1)
536
+ const metadata = visible.map(({ content: _, ...rest }) => rest);
537
+ return { content: [{ type: "text", text: JSON.stringify(metadata, null, 2) }] };
538
+ });
539
+ server.tool("get_skill", "Get a skill's full content by ID (progressive disclosure tier 2). Returns content + file list.", GetSkillSchema.shape, async (args) => {
540
+ const skill = await store.getSkill(args.skill_id);
541
+ if (!skill)
542
+ return ACCESS_DENIED;
543
+ // Check agent visibility (agents[] + selector)
544
+ const agent = await store.getAgent(agentId);
545
+ if (agent && skill.agents.length > 0) {
546
+ if (!skill.agents.some((a) => a.toLowerCase() === agent.name.toLowerCase())) {
547
+ return ACCESS_DENIED;
548
+ }
549
+ }
550
+ if (agent && skill.selector) {
551
+ if (skill.selector.type && skill.selector.type.toLowerCase() !== agent.type.toLowerCase())
552
+ return ACCESS_DENIED;
553
+ if (skill.selector.capability && !agent.capabilities.some((c) => c.toLowerCase() === skill.selector.capability.toLowerCase()))
554
+ return ACCESS_DENIED;
555
+ }
556
+ return { content: [{ type: "text", text: JSON.stringify(skill, null, 2) }] };
557
+ });
558
+ server.tool("delete_skill", "Delete a skill by ID. Creator or listed agents can delete.", DeleteSkillSchema.shape, async (args) => {
559
+ const skill = await store.getSkill(args.skill_id);
560
+ if (!skill)
561
+ return ACCESS_DENIED;
562
+ // Authorization: creator OR listed agent
563
+ if (skill.createdBy !== agentId) {
564
+ const agent = await store.getAgent(agentId);
565
+ const agentName = agent?.name ?? "";
566
+ if (!skill.agents.some((a) => a.toLowerCase() === agentName.toLowerCase())) {
567
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Only the creator or listed agents can delete this skill" }) }] };
568
+ }
569
+ }
570
+ const deleted = await store.deleteSkill(args.skill_id);
571
+ return { content: [{ type: "text", text: JSON.stringify({ deleted }) }] };
572
+ });
573
+ // --- Skill File tools ---
574
+ server.tool("set_skill_file", "Add or update a file attached to a skill. Creator or listed agents can manage files.", SetSkillFileSchema.shape, async (args) => {
575
+ const skill = await store.getSkill(args.skill_id);
576
+ if (!skill)
577
+ return ACCESS_DENIED;
578
+ // Authorization: creator OR listed agent
579
+ if (skill.createdBy !== agentId) {
580
+ const agent = await store.getAgent(agentId);
581
+ const agentName = agent?.name ?? "";
582
+ if (!skill.agents.some((a) => a.toLowerCase() === agentName.toLowerCase())) {
583
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Only the creator or listed agents can manage skill files" }) }] };
584
+ }
585
+ }
586
+ const file = await store.setSkillFile(args.skill_id, args.filename, args.content);
587
+ return { content: [{ type: "text", text: JSON.stringify(file, null, 2) }] };
588
+ });
589
+ server.tool("get_skill_file", "Get a file attached to a skill (progressive disclosure tier 3).", GetSkillFileSchema.shape, async (args) => {
590
+ // Check parent skill visibility first
591
+ const skill = await store.getSkill(args.skill_id);
592
+ if (!skill)
593
+ return ACCESS_DENIED;
594
+ const agent = await store.getAgent(agentId);
595
+ if (agent && skill.agents.length > 0) {
596
+ if (!skill.agents.some((a) => a.toLowerCase() === agent.name.toLowerCase())) {
597
+ return ACCESS_DENIED;
598
+ }
599
+ }
600
+ const file = await store.getSkillFile(args.skill_id, args.filename);
601
+ if (!file) {
602
+ return { content: [{ type: "text", text: JSON.stringify({ error: "File not found" }) }] };
603
+ }
604
+ return { content: [{ type: "text", text: JSON.stringify(file, null, 2) }] };
605
+ });
606
+ // --- Agent Memory tools ---
607
+ server.tool("set_agent_memory", "Save private memory for yourself. No scope = global. With project_id = per-project. With conversation_id = per-conversation. Content overwrites previous.", SetAgentMemorySchema.shape, async (args) => {
608
+ let scope = "global";
609
+ let scopeId;
610
+ if (args.conversation_id) {
611
+ scope = "conversation";
612
+ scopeId = args.conversation_id;
613
+ // Verify subscribed to the conversation
614
+ const subscribed = await store.isSubscribed(args.conversation_id, agentId);
615
+ if (!subscribed)
616
+ return ACCESS_DENIED;
617
+ }
618
+ else if (args.project_id) {
619
+ scope = "project";
620
+ scopeId = args.project_id;
621
+ // Verify project access
622
+ const project = await store.getProject(args.project_id, agentId);
623
+ if (!project)
624
+ return ACCESS_DENIED;
625
+ }
626
+ const mem = await store.setAgentMemory(agentId, scope, args.content, scopeId);
627
+ return { content: [{ type: "text", text: JSON.stringify(mem, null, 2) }] };
628
+ });
629
+ server.tool("get_agent_memory", "Read your private memory. No scope = global. With project_id = per-project. With conversation_id = per-conversation.", GetAgentMemorySchema.shape, async (args) => {
630
+ let scope = "global";
631
+ let scopeId;
632
+ if (args.conversation_id) {
633
+ scope = "conversation";
634
+ scopeId = args.conversation_id;
635
+ }
636
+ else if (args.project_id) {
637
+ scope = "project";
638
+ scopeId = args.project_id;
639
+ }
640
+ const mem = await store.getAgentMemory(agentId, scope, scopeId);
641
+ if (!mem) {
642
+ return { content: [{ type: "text", text: JSON.stringify({ content: null, scope }) }] };
643
+ }
644
+ return { content: [{ type: "text", text: JSON.stringify(mem, null, 2) }] };
645
+ });
646
+ server.tool("delete_agent_memory", "Delete your private memory for a scope. No scope = global. With project_id = per-project. With conversation_id = per-conversation.", DeleteAgentMemorySchema.shape, async (args) => {
647
+ let scope = "global";
648
+ let scopeId;
649
+ if (args.conversation_id) {
650
+ scope = "conversation";
651
+ scopeId = args.conversation_id;
652
+ }
653
+ else if (args.project_id) {
654
+ scope = "project";
655
+ scopeId = args.project_id;
656
+ }
657
+ const deleted = await store.deleteAgentMemory(agentId, scope, scopeId);
658
+ return { content: [{ type: "text", text: JSON.stringify({ deleted, scope }) }] };
659
+ });
660
+ // --- Status tools ---
315
661
  server.tool("get_status", "Get a summary: projects, active conversations, unread messages, online agents", GetStatusSchema.shape, async () => {
316
662
  const [projects, agents, unreadCount] = await Promise.all([
317
663
  store.listProjects(agentId),
@@ -535,6 +881,8 @@ export async function startBridgeServer(opts) {
535
881
  if (eventBusListeners && store.eventBus) {
536
882
  store.eventBus.offMessage(eventBusListeners.messageListener);
537
883
  store.eventBus.offAccessRequest(eventBusListeners.accessRequestListener);
884
+ store.eventBus.offTaskCreated(eventBusListeners.taskCreatedListener);
885
+ store.eventBus.offTaskUpdated(eventBusListeners.taskUpdatedListener);
538
886
  }
539
887
  // Close all active transports (best-effort — some may already be closed)
540
888
  for (const transport of transports.values()) {
@@ -587,10 +935,22 @@ function setupSSEDispatch(store) {
587
935
  log.error(`SSE access-request dispatch error: ${err instanceof Error ? err.message : String(err)}`);
588
936
  });
589
937
  };
938
+ const taskCreatedListener = (event) => {
939
+ dispatchTaskNotification(event.task, "created").catch((err) => {
940
+ log.error(`SSE task dispatch error: ${err instanceof Error ? err.message : String(err)}`);
941
+ });
942
+ };
943
+ const taskUpdatedListener = (event) => {
944
+ dispatchTaskNotification(event.task, event.action).catch((err) => {
945
+ log.error(`SSE task dispatch error: ${err instanceof Error ? err.message : String(err)}`);
946
+ });
947
+ };
590
948
  store.eventBus.onMessage(messageListener);
591
949
  store.eventBus.onAccessRequest(accessRequestListener);
950
+ store.eventBus.onTaskCreated(taskCreatedListener);
951
+ store.eventBus.onTaskUpdated(taskUpdatedListener);
592
952
  log.info("SSE push notifications enabled");
593
- return { messageListener, accessRequestListener };
953
+ return { messageListener, accessRequestListener, taskCreatedListener, taskUpdatedListener };
594
954
  }
595
955
  async function dispatchMessageNotification(store, event) {
596
956
  const { message } = event;
@@ -630,6 +990,9 @@ async function dispatchMessageNotification(store, event) {
630
990
  const agentVisInt = VISIBILITY_ORDER[agent.clearanceLevel];
631
991
  if (agentVisInt < messageVisInt)
632
992
  continue;
993
+ // Whisper gate: if message has recipients, agent must be in the list
994
+ if (message.recipients && !message.recipients.includes(sub.agentId))
995
+ continue;
633
996
  // Find all active sessions for this agent and push notification
634
997
  const sessions = agentSessions.get(sub.agentId);
635
998
  if (!sessions)
@@ -680,4 +1043,35 @@ async function dispatchAccessRequestNotification(store, event) {
680
1043
  }
681
1044
  }
682
1045
  }
1046
+ async function dispatchTaskNotification(task, action) {
1047
+ const notification = {
1048
+ jsonrpc: "2.0",
1049
+ method: "notifications/task",
1050
+ params: {
1051
+ taskId: task.id,
1052
+ projectId: task.projectId,
1053
+ conversationId: task.conversationId,
1054
+ title: task.title,
1055
+ status: task.status,
1056
+ action,
1057
+ createdBy: task.createdBy,
1058
+ claimedBy: task.claimedBy,
1059
+ updatedAt: task.updatedAt,
1060
+ },
1061
+ };
1062
+ // Project-level notification: push to all agents with active sessions
1063
+ for (const [, sessions] of agentSessions) {
1064
+ for (const sessionId of sessions) {
1065
+ const transport = transports.get(sessionId);
1066
+ if (!transport)
1067
+ continue;
1068
+ try {
1069
+ await transport.send(notification);
1070
+ }
1071
+ catch {
1072
+ log.debug(`SSE task send failed for session ${sessionId}`);
1073
+ }
1074
+ }
1075
+ }
1076
+ }
683
1077
  //# sourceMappingURL=server.js.map