agent-relay-server 0.43.0 → 0.45.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/docs/openapi.json +92 -1
- package/package.json +2 -2
- package/public/assets/{activity--LYUyJtT.js → activity-Dkzmef0Z.js} +2 -2
- package/public/assets/{activity--LYUyJtT.js.map → activity-Dkzmef0Z.js.map} +1 -1
- package/public/assets/{agent-profiles-BWKpVoNw.js → agent-profiles-DQYHMAyu.js} +2 -2
- package/public/assets/{agent-profiles-BWKpVoNw.js.map → agent-profiles-DQYHMAyu.js.map} +1 -1
- package/public/assets/{agents-6mhIWvF-.js → agents-atr6HZ1e.js} +2 -2
- package/public/assets/{agents-6mhIWvF-.js.map → agents-atr6HZ1e.js.map} +1 -1
- package/public/assets/{analytics-W-oZ8bsY.js → analytics-azFfAy4R.js} +2 -2
- package/public/assets/{analytics-W-oZ8bsY.js.map → analytics-azFfAy4R.js.map} +1 -1
- package/public/assets/{automation-DouRxEN7.js → automation-Bk4umHIm.js} +2 -2
- package/public/assets/{automation-DouRxEN7.js.map → automation-Bk4umHIm.js.map} +1 -1
- package/public/assets/{branch-state-badge-dN2mQ2gs.js → branch-state-badge-DymObp8d.js} +2 -2
- package/public/assets/{branch-state-badge-dN2mQ2gs.js.map → branch-state-badge-DymObp8d.js.map} +1 -1
- package/public/assets/channels-B_0wDy0s.js +2 -0
- package/public/assets/{channels-CQy-JL7o.js.map → channels-B_0wDy0s.js.map} +1 -1
- package/public/assets/chat-DxV4MyYF.js +2 -0
- package/public/assets/chat-DxV4MyYF.js.map +1 -0
- package/public/assets/{connectors-ByNxzq2k.js → connectors-DLqYF2_3.js} +2 -2
- package/public/assets/{connectors-ByNxzq2k.js.map → connectors-DLqYF2_3.js.map} +1 -1
- package/public/assets/display-ConJ9cJB.js.map +1 -1
- package/public/assets/{formatted-body-impl-CthzMcxz.js → formatted-body-impl-LK0QcFLC.js} +2 -2
- package/public/assets/{formatted-body-impl-CthzMcxz.js.map → formatted-body-impl-LK0QcFLC.js.map} +1 -1
- package/public/assets/{index-B1s-hGRR.js → index-CT10XH5Y.js} +8 -8
- package/public/assets/{index-B1s-hGRR.js.map → index-CT10XH5Y.js.map} +1 -1
- package/public/assets/{integrations-BBmL-PnQ.js → integrations-CdeQuRSQ.js} +2 -2
- package/public/assets/{integrations-BBmL-PnQ.js.map → integrations-CdeQuRSQ.js.map} +1 -1
- package/public/assets/{maintenance-CdDtYHE9.js → maintenance-DbqSE_ps.js} +2 -2
- package/public/assets/{maintenance-CdDtYHE9.js.map → maintenance-DbqSE_ps.js.map} +1 -1
- package/public/assets/{managed-agents-DXigb0pj.js → managed-agents-BxhRiBVn.js} +2 -2
- package/public/assets/{managed-agents-DXigb0pj.js.map → managed-agents-BxhRiBVn.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BVES_oc1.js → markdown-preview-impl-Dz2Spmat.js} +2 -2
- package/public/assets/{markdown-preview-impl-BVES_oc1.js.map → markdown-preview-impl-Dz2Spmat.js.map} +1 -1
- package/public/assets/{memory-DWAF794H.js → memory-BTjUKTyA.js} +2 -2
- package/public/assets/{memory-DWAF794H.js.map → memory-BTjUKTyA.js.map} +1 -1
- package/public/assets/{messages-D63r1IRE.js → messages-C8z05Y3O.js} +2 -2
- package/public/assets/{messages-D63r1IRE.js.map → messages-C8z05Y3O.js.map} +1 -1
- package/public/assets/{orchestrators-rbEOs7pu.js → orchestrators-DQqtYryc.js} +2 -2
- package/public/assets/{orchestrators-rbEOs7pu.js.map → orchestrators-DQqtYryc.js.map} +1 -1
- package/public/assets/{overview-CeUZzyTa.js → overview-CGYu6qQr.js} +2 -2
- package/public/assets/{overview-CeUZzyTa.js.map → overview-CGYu6qQr.js.map} +1 -1
- package/public/assets/pairs-BgqXzCto.js +2 -0
- package/public/assets/{pairs-DJ_e1R5S.js.map → pairs-BgqXzCto.js.map} +1 -1
- package/public/assets/{security-DyeypCyX.js → security-BqfRVw-p.js} +2 -2
- package/public/assets/{security-DyeypCyX.js.map → security-BqfRVw-p.js.map} +1 -1
- package/public/assets/{settings-CVW329oA.js → settings-63whklIq.js} +3 -3
- package/public/assets/{settings-CVW329oA.js.map → settings-63whklIq.js.map} +1 -1
- package/public/assets/store-B6qT5_de.js +9 -0
- package/public/assets/store-B6qT5_de.js.map +1 -0
- package/public/assets/tasks-DiZ_4TYW.js +2 -0
- package/public/assets/{tasks-CR8BxsPc.js.map → tasks-DiZ_4TYW.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-CISUjIl9.js → terminal-viewer-impl-D3T1F77z.js} +2 -2
- package/public/assets/{terminal-viewer-impl-CISUjIl9.js.map → terminal-viewer-impl-D3T1F77z.js.map} +1 -1
- package/public/assets/work-queue-C1p2Hybt.js +2 -0
- package/public/assets/{work-queue-ipT9bYqc.js.map → work-queue-C1p2Hybt.js.map} +1 -1
- package/public/assets/{workspaces-BD1P6J21.js → workspaces-DqVr0uN7.js} +2 -2
- package/public/assets/{workspaces-BD1P6J21.js.map → workspaces-DqVr0uN7.js.map} +1 -1
- package/public/index.html +2 -2
- package/src/db/blackboard.ts +171 -0
- package/src/db/index.ts +1 -0
- package/src/db/migrations.ts +37 -1
- package/src/db/schema.ts +16 -1
- package/src/mcp-blackboard-tools.ts +131 -0
- package/src/mcp-plan-tools.ts +6 -3
- package/src/mcp.ts +3 -0
- package/src/routes/blackboard.ts +54 -0
- package/src/routes/index.ts +3 -0
- package/src/routes/plans.ts +9 -3
- package/src/security.ts +2 -0
- package/src/services/plan-graph.ts +16 -4
- package/src/token-db.ts +3 -3
- package/public/assets/channels-CQy-JL7o.js +0 -2
- package/public/assets/chat-DVu6bXmE.js +0 -2
- package/public/assets/chat-DVu6bXmE.js.map +0 -1
- package/public/assets/pairs-DJ_e1R5S.js +0 -2
- package/public/assets/store-CtKmqMfx.js +0 -9
- package/public/assets/store-CtKmqMfx.js.map +0 -1
- package/public/assets/tasks-CR8BxsPc.js +0 -2
- package/public/assets/work-queue-ipT9bYqc.js +0 -2
package/src/db/migrations.ts
CHANGED
|
@@ -302,6 +302,23 @@ export function applyMigrations(): void {
|
|
|
302
302
|
`);
|
|
303
303
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_teams_coordinator ON teams(coordinator_id)");
|
|
304
304
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_teams_status ON teams(status)");
|
|
305
|
+
getDb().run(`
|
|
306
|
+
CREATE TABLE IF NOT EXISTS blackboard_entries (
|
|
307
|
+
id TEXT PRIMARY KEY,
|
|
308
|
+
team_id TEXT NOT NULL,
|
|
309
|
+
kind TEXT NOT NULL,
|
|
310
|
+
title TEXT NOT NULL,
|
|
311
|
+
body TEXT,
|
|
312
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
313
|
+
supersedes TEXT,
|
|
314
|
+
tags TEXT,
|
|
315
|
+
author_id TEXT NOT NULL,
|
|
316
|
+
created_at INTEGER NOT NULL,
|
|
317
|
+
updated_at INTEGER NOT NULL
|
|
318
|
+
)
|
|
319
|
+
`);
|
|
320
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_blackboard_team ON blackboard_entries(team_id)");
|
|
321
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_blackboard_team_kind ON blackboard_entries(team_id, kind)");
|
|
305
322
|
getDb().run(`
|
|
306
323
|
CREATE TABLE IF NOT EXISTS plans (
|
|
307
324
|
id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, team_id TEXT, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
|
|
@@ -319,11 +336,30 @@ export function applyMigrations(): void {
|
|
|
319
336
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_plans_team ON plans(team_id)");
|
|
320
337
|
getDb().run(`
|
|
321
338
|
CREATE TABLE IF NOT EXISTS plan_versions (
|
|
322
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id TEXT NOT NULL
|
|
339
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id TEXT NOT NULL,
|
|
323
340
|
version INTEGER NOT NULL, title TEXT, objective TEXT, nodes TEXT NOT NULL, edges TEXT NOT NULL,
|
|
324
341
|
changed_by TEXT, changed_at INTEGER NOT NULL
|
|
325
342
|
)
|
|
326
343
|
`);
|
|
344
|
+
const planVersionFks = getDb().query("PRAGMA foreign_key_list(plan_versions)").all() as any[];
|
|
345
|
+
if (planVersionFks.some((fk) => fk.table === "plans" && String(fk.on_delete).toUpperCase() === "CASCADE")) {
|
|
346
|
+
getDb().transaction(() => {
|
|
347
|
+
getDb().run("ALTER TABLE plan_versions RENAME TO plan_versions_old");
|
|
348
|
+
getDb().run(`
|
|
349
|
+
CREATE TABLE plan_versions (
|
|
350
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id TEXT NOT NULL,
|
|
351
|
+
version INTEGER NOT NULL, title TEXT, objective TEXT, nodes TEXT NOT NULL, edges TEXT NOT NULL,
|
|
352
|
+
changed_by TEXT, changed_at INTEGER NOT NULL
|
|
353
|
+
)
|
|
354
|
+
`);
|
|
355
|
+
getDb().run(`
|
|
356
|
+
INSERT INTO plan_versions (id, plan_id, version, title, objective, nodes, edges, changed_by, changed_at)
|
|
357
|
+
SELECT id, plan_id, version, title, objective, nodes, edges, changed_by, changed_at
|
|
358
|
+
FROM plan_versions_old
|
|
359
|
+
`);
|
|
360
|
+
getDb().run("DROP TABLE plan_versions_old");
|
|
361
|
+
})();
|
|
362
|
+
}
|
|
327
363
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_plan_versions_plan ON plan_versions(plan_id, version DESC)");
|
|
328
364
|
|
|
329
365
|
// Backfill thread_id for pre-migration rows (self-threaded).
|
package/src/db/schema.ts
CHANGED
|
@@ -171,6 +171,21 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
171
171
|
);
|
|
172
172
|
CREATE INDEX IF NOT EXISTS idx_teams_coordinator ON teams(coordinator_id);
|
|
173
173
|
CREATE INDEX IF NOT EXISTS idx_teams_status ON teams(status);
|
|
174
|
+
CREATE TABLE IF NOT EXISTS blackboard_entries (
|
|
175
|
+
id TEXT PRIMARY KEY,
|
|
176
|
+
team_id TEXT NOT NULL,
|
|
177
|
+
kind TEXT NOT NULL,
|
|
178
|
+
title TEXT NOT NULL,
|
|
179
|
+
body TEXT,
|
|
180
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
181
|
+
supersedes TEXT,
|
|
182
|
+
tags TEXT,
|
|
183
|
+
author_id TEXT NOT NULL,
|
|
184
|
+
created_at INTEGER NOT NULL,
|
|
185
|
+
updated_at INTEGER NOT NULL
|
|
186
|
+
);
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_blackboard_team ON blackboard_entries(team_id);
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_blackboard_team_kind ON blackboard_entries(team_id, kind);
|
|
174
189
|
CREATE TABLE IF NOT EXISTS plans (
|
|
175
190
|
id TEXT PRIMARY KEY, thread_id INTEGER NOT NULL, team_id TEXT, channel TEXT, version INTEGER NOT NULL DEFAULT 1,
|
|
176
191
|
title TEXT, objective TEXT, nodes TEXT NOT NULL DEFAULT '[]', edges TEXT NOT NULL DEFAULT '[]',
|
|
@@ -179,7 +194,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
179
194
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_thread_channel ON plans(thread_id, coalesce(channel, ''));
|
|
180
195
|
CREATE INDEX IF NOT EXISTS idx_plans_updated ON plans(updated_at);
|
|
181
196
|
CREATE TABLE IF NOT EXISTS plan_versions (
|
|
182
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id TEXT NOT NULL
|
|
197
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, plan_id TEXT NOT NULL,
|
|
183
198
|
version INTEGER NOT NULL, title TEXT, objective TEXT, nodes TEXT NOT NULL, edges TEXT NOT NULL,
|
|
184
199
|
changed_by TEXT, changed_at INTEGER NOT NULL
|
|
185
200
|
);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { getAgent, getEntry, getEntryChain, listEntries, retractEntry, searchEntries, ValidationError, writeEntry } from "./db";
|
|
2
|
+
import { McpNotFoundError } from "./mcp-errors";
|
|
3
|
+
import { authContextFromMcp } from "./services/auth-context";
|
|
4
|
+
import { cleanString, cleanStringArray, optionalEnum } from "./validation";
|
|
5
|
+
import type { BlackboardEntryKind } from "./types";
|
|
6
|
+
|
|
7
|
+
const BLACKBOARD_KINDS = ["decision", "fact", "ruled_out"] as const;
|
|
8
|
+
|
|
9
|
+
export const BLACKBOARD_TOOLS = [
|
|
10
|
+
{
|
|
11
|
+
name: "relay_blackboard_write",
|
|
12
|
+
description: "Create a team-scoped blackboard entry. The caller's token identity determines the team and author.",
|
|
13
|
+
requiredScopes: ["blackboard:write"],
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
kind: { type: "string", enum: BLACKBOARD_KINDS },
|
|
18
|
+
title: { type: "string" },
|
|
19
|
+
body: { type: "string" },
|
|
20
|
+
tags: { type: "array", items: { type: "string" } },
|
|
21
|
+
supersedes: { type: "string" },
|
|
22
|
+
},
|
|
23
|
+
required: ["kind", "title"],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "relay_blackboard_get",
|
|
29
|
+
description: "Fetch one blackboard entry and its supersession chain for the caller's team.",
|
|
30
|
+
requiredScopes: ["blackboard:read"],
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: { id: { type: "string" } },
|
|
34
|
+
required: ["id"],
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "relay_blackboard_read",
|
|
40
|
+
description: "Read the caller team's active blackboard working set, optionally filtered by kind.",
|
|
41
|
+
requiredScopes: ["blackboard:read"],
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: { kind: { type: "string", enum: BLACKBOARD_KINDS } },
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "relay_blackboard_search",
|
|
50
|
+
description: "Search active blackboard entries for the caller's team by title, body, or tags.",
|
|
51
|
+
requiredScopes: ["blackboard:read"],
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
query: { type: "string" },
|
|
56
|
+
kind: { type: "string", enum: BLACKBOARD_KINDS },
|
|
57
|
+
},
|
|
58
|
+
required: ["query"],
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "relay_blackboard_retract",
|
|
64
|
+
description: "Retract a blackboard entry for the caller's team without deleting its ledger row.",
|
|
65
|
+
requiredScopes: ["blackboard:write"],
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: { id: { type: "string" } },
|
|
69
|
+
required: ["id"],
|
|
70
|
+
additionalProperties: false,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
] satisfies Array<{ name: string; description: string; requiredScopes: string[]; inputSchema: Record<string, unknown> }>;
|
|
74
|
+
|
|
75
|
+
type McpAuthLike = Parameters<typeof authContextFromMcp>[0];
|
|
76
|
+
|
|
77
|
+
function callerTeam(auth: McpAuthLike): { agentId: string; teamId: string } {
|
|
78
|
+
const ctx = authContextFromMcp(auth);
|
|
79
|
+
if (!ctx.callerAgentId) throw new ValidationError("blackboard operations require a single caller agent identity");
|
|
80
|
+
const agent = getAgent(ctx.callerAgentId);
|
|
81
|
+
if (!agent?.teamId) throw new ValidationError("caller has no team");
|
|
82
|
+
return { agentId: ctx.callerAgentId, teamId: agent.teamId };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cleanKind(value: unknown): BlackboardEntryKind | undefined {
|
|
86
|
+
return optionalEnum(value, "kind", BLACKBOARD_KINDS) as BlackboardEntryKind | undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ensureSameTeam(entryId: string, teamId: string): void {
|
|
90
|
+
const entry = getEntry(entryId);
|
|
91
|
+
if (!entry) throw new McpNotFoundError("blackboard entry not found");
|
|
92
|
+
if (entry.teamId !== teamId) throw new McpNotFoundError("blackboard entry not found");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function relayBlackboardTool(auth: McpAuthLike, name: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
96
|
+
const { agentId, teamId } = callerTeam(auth);
|
|
97
|
+
if (name === "relay_blackboard_write") {
|
|
98
|
+
const supersedes = cleanString(args.supersedes, "supersedes", { max: 120 });
|
|
99
|
+
const kind = cleanKind(args.kind);
|
|
100
|
+
if (!kind) throw new ValidationError("kind required");
|
|
101
|
+
if (supersedes) ensureSameTeam(supersedes, teamId);
|
|
102
|
+
return writeEntry({
|
|
103
|
+
teamId,
|
|
104
|
+
authorId: agentId,
|
|
105
|
+
kind,
|
|
106
|
+
title: cleanString(args.title, "title", { required: true, max: 240 })!,
|
|
107
|
+
body: cleanString(args.body, "body", { max: 16_000 }),
|
|
108
|
+
tags: cleanStringArray(args.tags, "tags", { itemMax: 80, maxItems: 32 }),
|
|
109
|
+
supersedes,
|
|
110
|
+
}) as unknown as Record<string, unknown>;
|
|
111
|
+
}
|
|
112
|
+
if (name === "relay_blackboard_get") {
|
|
113
|
+
const id = cleanString(args.id, "id", { required: true, max: 120 })!;
|
|
114
|
+
ensureSameTeam(id, teamId);
|
|
115
|
+
return { entry: getEntry(id), chain: getEntryChain(id) };
|
|
116
|
+
}
|
|
117
|
+
if (name === "relay_blackboard_read") {
|
|
118
|
+
return { entries: listEntries(teamId, { kind: cleanKind(args.kind) }) };
|
|
119
|
+
}
|
|
120
|
+
if (name === "relay_blackboard_search") {
|
|
121
|
+
return { entries: searchEntries(teamId, cleanString(args.query, "query", { required: true, max: 240 })!, { kind: cleanKind(args.kind) }) };
|
|
122
|
+
}
|
|
123
|
+
if (name === "relay_blackboard_retract") {
|
|
124
|
+
const id = cleanString(args.id, "id", { required: true, max: 120 })!;
|
|
125
|
+
ensureSameTeam(id, teamId);
|
|
126
|
+
const entry = retractEntry(id);
|
|
127
|
+
if (!entry) throw new McpNotFoundError("blackboard entry not found");
|
|
128
|
+
return entry as unknown as Record<string, unknown>;
|
|
129
|
+
}
|
|
130
|
+
throw new ValidationError(`unknown tool: ${name}`);
|
|
131
|
+
}
|
package/src/mcp-plan-tools.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
addPlanEdge,
|
|
6
6
|
addPlanNode,
|
|
7
7
|
createPlan,
|
|
8
|
+
deletePlanGraph,
|
|
8
9
|
getPlanGraph,
|
|
9
10
|
removePlanEdge,
|
|
10
11
|
removePlanNode,
|
|
@@ -82,13 +83,14 @@ export const PLAN_TOOLS = [
|
|
|
82
83
|
{ name: "relay_plan_add_edge", description: "Add a dependency edge to a plan DAG; rejects cycles.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" }, from: { type: "string" }, to: { type: "string" } }, required: ["planId", "from", "to"], additionalProperties: false } },
|
|
83
84
|
{ name: "relay_plan_remove_node", description: "Remove a node and its connected edges from a plan DAG.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" }, nodeId: { type: "string" } }, required: ["planId", "nodeId"], additionalProperties: false } },
|
|
84
85
|
{ name: "relay_plan_remove_edge", description: "Remove a dependency edge from a plan DAG.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" }, from: { type: "string" }, to: { type: "string" } }, required: ["planId", "from", "to"], additionalProperties: false } },
|
|
86
|
+
{ name: "relay_plan_delete", description: "Delete a persisted plan DAG and archive its latest version.", requiredScopes: ["plans:write"], inputSchema: { type: "object", properties: { planId: { type: "string" } }, required: ["planId"], additionalProperties: false } },
|
|
85
87
|
{
|
|
86
88
|
name: "relay_plan_get",
|
|
87
|
-
description: "Fetch a plan by planId or by threadId/channel.",
|
|
89
|
+
description: "Fetch a plan by planId, teamId, or by threadId/channel.",
|
|
88
90
|
requiredScopes: ["plans:read"],
|
|
89
91
|
inputSchema: {
|
|
90
92
|
type: "object",
|
|
91
|
-
properties: { planId: { type: "string" }, threadId: { oneOf: [{ type: "string" }, { type: "integer", minimum: 1 }] }, channel: { type: "string" } },
|
|
93
|
+
properties: { planId: { type: "string" }, teamId: { type: "string" }, threadId: { oneOf: [{ type: "string" }, { type: "integer", minimum: 1 }] }, channel: { type: "string" } },
|
|
92
94
|
additionalProperties: false,
|
|
93
95
|
},
|
|
94
96
|
},
|
|
@@ -112,8 +114,9 @@ export function relayPlanTool(auth: McpAuthLike, name: string, args: Record<stri
|
|
|
112
114
|
if (name === "relay_plan_add_edge") return addPlanEdge(stringArg(args.planId, "planId"), { from: args.from, to: args.to }, ctx) as unknown as Record<string, unknown>;
|
|
113
115
|
if (name === "relay_plan_remove_node") return removePlanNode(stringArg(args.planId, "planId"), stringArg(args.nodeId, "nodeId"), ctx) as unknown as Record<string, unknown>;
|
|
114
116
|
if (name === "relay_plan_remove_edge") return removePlanEdge(stringArg(args.planId, "planId"), { from: args.from, to: args.to }, ctx) as unknown as Record<string, unknown>;
|
|
117
|
+
if (name === "relay_plan_delete") return deletePlanGraph(stringArg(args.planId, "planId"), ctx);
|
|
115
118
|
if (name === "relay_plan_get") {
|
|
116
|
-
const plan = getPlanGraph({ planId: args.planId, threadId: args.threadId, channel: args.channel });
|
|
119
|
+
const plan = getPlanGraph({ planId: args.planId, teamId: args.teamId, threadId: args.threadId, channel: args.channel });
|
|
117
120
|
if (!plan) throw new McpNotFoundError("plan not found");
|
|
118
121
|
return plan as unknown as Record<string, unknown>;
|
|
119
122
|
}
|
package/src/mcp.ts
CHANGED
|
@@ -48,6 +48,7 @@ import { sendMessageService } from "./services/send-message";
|
|
|
48
48
|
import { ServiceAuthError } from "./services/errors";
|
|
49
49
|
import { PLAN_TOOLS, relayPlanTool } from "./mcp-plan-tools";
|
|
50
50
|
import { TEAM_TOOLS, relayTeamTool } from "./mcp-team-tools";
|
|
51
|
+
import { BLACKBOARD_TOOLS, relayBlackboardTool } from "./mcp-blackboard-tools";
|
|
51
52
|
import type { ActivityKind, ArtifactKind, ArtifactSensitivity, AttachmentRef, Command, SendMessageInput, Message, SpawnApprovalMode, SpawnProvider, WorkspaceAutoMergePolicy, WorkspaceMergeStrategy, WorkspaceMode, WorkspaceRecord } from "./types";
|
|
52
53
|
import { LAND_STRATEGIES, applyWorkspaceAction, waitForWorkspaceStatus, type WorkspaceAction } from "./workspace-actions";
|
|
53
54
|
import { AUTO_MERGE_POLICIES } from "./workspace-merge";
|
|
@@ -460,6 +461,7 @@ const TOOLS: ToolDefinition[] = [
|
|
|
460
461
|
},
|
|
461
462
|
...TEAM_TOOLS,
|
|
462
463
|
...PLAN_TOOLS,
|
|
464
|
+
...BLACKBOARD_TOOLS,
|
|
463
465
|
];
|
|
464
466
|
|
|
465
467
|
export async function postMcp(req: Request): Promise<Response> {
|
|
@@ -573,6 +575,7 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
|
|
|
573
575
|
else if (name === "relay_workspace_land") result = relayWorkspaceMutation(auth, "merge", args);
|
|
574
576
|
else if (name.startsWith("relay_team_")) result = relayTeamTool(auth, name, args);
|
|
575
577
|
else if (name.startsWith("relay_plan_")) result = relayPlanTool(auth, name, args);
|
|
578
|
+
else if (name.startsWith("relay_blackboard_")) result = relayBlackboardTool(auth, name, args);
|
|
576
579
|
else throw new ValidationError(`unknown tool: ${name}`);
|
|
577
580
|
|
|
578
581
|
auditToolCall(auth, name, "ok", result);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getAgent, getEntry, getEntryChain, listEntries } from "../db";
|
|
2
|
+
import { authContextFromRequest } from "../services/auth-context";
|
|
3
|
+
import { cleanString, optionalEnum } from "../validation";
|
|
4
|
+
import { error, json, type Handler } from "./_shared";
|
|
5
|
+
import type { BlackboardEntryKind } from "../types";
|
|
6
|
+
|
|
7
|
+
const BLACKBOARD_KINDS = ["decision", "fact", "ruled_out"] as const;
|
|
8
|
+
|
|
9
|
+
function isAdminContext(ctx: ReturnType<typeof authContextFromRequest>): boolean {
|
|
10
|
+
return ctx.scopes.includes("*") || ctx.scopes.includes("admin:*");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function routeTeamId(req: Request): string | Response {
|
|
14
|
+
const ctx = authContextFromRequest(req);
|
|
15
|
+
if (ctx.component && ctx.callerAgentId) {
|
|
16
|
+
const callerId = ctx.callerAgentId;
|
|
17
|
+
const agent = getAgent(callerId);
|
|
18
|
+
return agent?.teamId ?? error("caller has no team", 403);
|
|
19
|
+
}
|
|
20
|
+
if (ctx.component && !isAdminContext(ctx)) return error("caller has no team", 403);
|
|
21
|
+
const teamId = cleanString(new URL(req.url).searchParams.get("team") ?? undefined, "team", { required: true, max: 120 });
|
|
22
|
+
return teamId!;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function routeKind(req: Request): BlackboardEntryKind | undefined {
|
|
26
|
+
return optionalEnum(new URL(req.url).searchParams.get("kind") ?? undefined, "kind", BLACKBOARD_KINDS) as BlackboardEntryKind | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const getBlackboardRoute: Handler = (req) => {
|
|
30
|
+
try {
|
|
31
|
+
const teamId = routeTeamId(req);
|
|
32
|
+
if (teamId instanceof Response) return teamId;
|
|
33
|
+
return json({ entries: listEntries(teamId, { kind: routeKind(req) }) });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return error(err instanceof Error ? err.message : "blackboard route failed");
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const getBlackboardEntryRoute: Handler = (req, params) => {
|
|
40
|
+
try {
|
|
41
|
+
const id = cleanString(params.id, "id", { required: true, max: 120 })!;
|
|
42
|
+
const entry = getEntry(id);
|
|
43
|
+
if (!entry) return error("blackboard entry not found", 404);
|
|
44
|
+
const ctx = authContextFromRequest(req);
|
|
45
|
+
if (ctx.component && ctx.callerAgentId) {
|
|
46
|
+
const agent = getAgent(ctx.callerAgentId);
|
|
47
|
+
if (!agent?.teamId || agent.teamId !== entry.teamId) return error("blackboard entry not found", 404);
|
|
48
|
+
}
|
|
49
|
+
if (ctx.component && !ctx.callerAgentId && !isAdminContext(ctx)) return error("blackboard entry not found", 404);
|
|
50
|
+
return json({ entry, chain: getEntryChain(id) });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return error(err instanceof Error ? err.message : "blackboard route failed");
|
|
53
|
+
}
|
|
54
|
+
};
|
package/src/routes/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { getInsightsConfigRoute, getInsightsObservationsRoute, postInsightsObser
|
|
|
27
27
|
import { getOrchestratorDirectories, getOrchestratorFileRead, getOrchestratorFileStat, getOrchestratorFilesList, getOrchestratorLogs, getOrchestratorProviders, getOrchestratorSessions, getOrchestratorTerminal, getOrchestratorVersion, getOrchestratorWorkspaceProbe, postOrchestratorCreateDirectory, postOrchestratorTerminalInput, postOrchestratorTerminalResize } from "./orchestrator-proxy";
|
|
28
28
|
import { getPairById, getPairs, postAcceptPair, postHangupPair, postPair, postPairMessage, postRejectPair } from "./pairs";
|
|
29
29
|
import { getPlanByIdRoute, getPlansRoute } from "./plans";
|
|
30
|
+
import { getBlackboardEntryRoute, getBlackboardRoute } from "./blackboard";
|
|
30
31
|
import { getRecipeByName, getRecipeInstanceArtifacts, getRecipeInstanceById, getRecipeInstances, getRecipes, postRecipeInstanceArtifacts, postRecipeStart, postRecipeStop } from "./recipes";
|
|
31
32
|
import { getStewardConfigRoute, getWorkspaceConfigRoute, putStewardConfigRoute, putWorkspaceConfigRoute } from "./steward";
|
|
32
33
|
import { getTaskById, getTaskEvents, getTasks, patchTaskStatus, postClaimTask, postRenewTaskClaim } from "./tasks";
|
|
@@ -267,6 +268,8 @@ const routes: Route[] = [
|
|
|
267
268
|
route("POST", "/api/pairs/:id/messages", postPairMessage),
|
|
268
269
|
route("GET", "/api/plans", getPlansRoute),
|
|
269
270
|
route("GET", "/api/plans/:id", getPlanByIdRoute),
|
|
271
|
+
route("GET", "/api/blackboard", getBlackboardRoute),
|
|
272
|
+
route("GET", "/api/blackboard/:id", getBlackboardEntryRoute),
|
|
270
273
|
|
|
271
274
|
route("GET", "/api/spec", getApiSpec),
|
|
272
275
|
route("GET", "/api/docs", getApiDocs),
|
package/src/routes/plans.ts
CHANGED
|
@@ -5,10 +5,16 @@ import { error, json, parseQueryInt, type Handler } from "./_shared";
|
|
|
5
5
|
|
|
6
6
|
export const getPlansRoute: Handler = (req) => {
|
|
7
7
|
const url = new URL(req.url);
|
|
8
|
-
const threadId = parseQueryInt(url.searchParams.get("threadId"), { min: 1, max: Number.MAX_SAFE_INTEGER });
|
|
9
|
-
if (threadId === null) return error("threadId required");
|
|
10
|
-
if (Number.isNaN(threadId)) return error("threadId must be a positive integer");
|
|
11
8
|
const channel = cleanString(url.searchParams.get("channel") ?? undefined, "channel", { max: 120 });
|
|
9
|
+
const teamId = cleanString(url.searchParams.get("teamId") ?? undefined, "teamId", { max: 120 });
|
|
10
|
+
if (teamId) {
|
|
11
|
+
const plan = getPlanGraph({ teamId, channel });
|
|
12
|
+
return plan ? json(plan) : error("plan not found", 404);
|
|
13
|
+
}
|
|
14
|
+
const rawThreadId = url.searchParams.get("threadId");
|
|
15
|
+
if (rawThreadId === null) return error("threadId or teamId required");
|
|
16
|
+
const threadId = parseQueryInt(rawThreadId, { min: 1, max: Number.MAX_SAFE_INTEGER });
|
|
17
|
+
if (Number.isNaN(threadId)) return error("threadId must be a positive integer");
|
|
12
18
|
const plan = getPlanGraph({ threadId, channel });
|
|
13
19
|
return plan ? json(plan) : error("plan not found", 404);
|
|
14
20
|
};
|
package/src/security.ts
CHANGED
|
@@ -205,6 +205,7 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
|
|
|
205
205
|
if (pathname.startsWith("/api/tasks")) return method === "GET" ? "task:read" : "task:write";
|
|
206
206
|
if (pathname.startsWith("/api/pairs")) return method === "GET" ? "pairs:read" : "pairs:write";
|
|
207
207
|
if (pathname.startsWith("/api/teams")) return method === "GET" ? "teams:read" : "teams:write";
|
|
208
|
+
if (pathname.startsWith("/api/blackboard")) return method === "GET" ? "blackboard:read" : "blackboard:write";
|
|
208
209
|
// Insights config (the feature toggle) stays admin-only via the default; only the
|
|
209
210
|
// mechanical observation feed is writable by lower-privilege callers.
|
|
210
211
|
if (pathname === "/api/insights/observations") return method === "GET" ? "insights:read" : "insights:write";
|
|
@@ -280,6 +281,7 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
280
281
|
if (pathname.startsWith("/api/orchestrators")) return method === "GET" ? "agent:read" : "command:write";
|
|
281
282
|
if (pathname.startsWith("/api/plans")) return method === "GET" ? "plans:read" : "plans:write";
|
|
282
283
|
if (pathname.startsWith("/api/teams")) return method === "GET" ? "teams:read" : "teams:write";
|
|
284
|
+
if (pathname.startsWith("/api/blackboard")) return method === "GET" ? "blackboard:read" : "blackboard:write";
|
|
283
285
|
// The Runner posts the #184 context-gathering signal here (source:"server") via its
|
|
284
286
|
// provider token. Without this case the path fell through to the system:admin default
|
|
285
287
|
// below and every observation was 403-dropped — the whole Insights feed stayed empty.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isRecord } from "agent-relay-sdk";
|
|
2
|
-
import { createPlanRow, getPlan, getPlanByThread, planThreadExists, replacePlan, ValidationError } from "../db";
|
|
2
|
+
import { createPlanRow, deletePlan, getAgent, getPlan, getPlanByTeam, getPlanByThread, planThreadExists, replacePlan, ValidationError } from "../db";
|
|
3
3
|
import { emitRelayEvent } from "../events";
|
|
4
4
|
import { cleanString, optionalEnum } from "../validation";
|
|
5
5
|
import type { AuthContext } from "./auth-context";
|
|
@@ -173,7 +173,7 @@ function existingPlan(id: string): Plan {
|
|
|
173
173
|
return plan;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
function emitPlan(type: "plan.created" | "plan.changed", plan: Plan, actor: string): void {
|
|
176
|
+
function emitPlan(type: "plan.created" | "plan.changed" | "plan.deleted", plan: Plan, actor: string): void {
|
|
177
177
|
emitRelayEvent({ type, source: actor, subject: plan.id, data: { plan, actor } });
|
|
178
178
|
}
|
|
179
179
|
|
|
@@ -185,9 +185,11 @@ export function createPlan(input: PlanCreateInput, ctx: AuthContext): Plan {
|
|
|
185
185
|
const edges = cleanEdges(input.edges);
|
|
186
186
|
validateEdges(nodes, edges);
|
|
187
187
|
const actor = actorId(ctx);
|
|
188
|
+
const explicitTeamId = cleanString(input.teamId, "teamId", { max: 120 });
|
|
189
|
+
const teamId = explicitTeamId ?? (input.teamId === undefined || input.teamId === null ? getAgent(actor)?.teamId : undefined);
|
|
188
190
|
const plan = createPlanRow({
|
|
189
191
|
threadId,
|
|
190
|
-
teamId
|
|
192
|
+
teamId,
|
|
191
193
|
channel,
|
|
192
194
|
title: cleanString(input.title, "title", { max: 240 }),
|
|
193
195
|
objective: cleanString(input.objective, "objective", { max: 4000 }),
|
|
@@ -264,9 +266,19 @@ export function removePlanEdge(planId: string, edgeInput: unknown, ctx: AuthCont
|
|
|
264
266
|
return updated;
|
|
265
267
|
}
|
|
266
268
|
|
|
267
|
-
export function
|
|
269
|
+
export function deletePlanGraph(planId: string, ctx: AuthContext): { id: string; deleted: true } {
|
|
270
|
+
const plan = existingPlan(planId);
|
|
271
|
+
const actor = actorId(ctx);
|
|
272
|
+
deletePlan(plan.id, actor);
|
|
273
|
+
emitPlan("plan.deleted", plan, actor);
|
|
274
|
+
return { id: plan.id, deleted: true };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function getPlanGraph(input: { planId?: unknown; threadId?: unknown; teamId?: unknown; channel?: unknown }): Plan | null {
|
|
268
278
|
const planId = cleanString(input.planId, "planId", { max: 120 });
|
|
269
279
|
if (planId) return getPlan(planId);
|
|
280
|
+
const teamId = cleanString(input.teamId, "teamId", { max: 120 });
|
|
281
|
+
if (teamId) return getPlanByTeam(teamId, cleanString(input.channel, "channel", { max: 120 }));
|
|
270
282
|
const threadId = cleanThreadId(input.threadId);
|
|
271
283
|
return getPlanByThread(threadId, cleanString(input.channel, "channel", { max: 120 }));
|
|
272
284
|
}
|
package/src/token-db.ts
CHANGED
|
@@ -57,7 +57,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
57
57
|
name: "Provider Agent",
|
|
58
58
|
description: "Coding-agent runtime access for messages, commands, tasks, and scoped memory reads.",
|
|
59
59
|
role: "provider",
|
|
60
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "insights:write"],
|
|
60
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "insights:write"],
|
|
61
61
|
ttlSeconds: 24 * 60 * 60,
|
|
62
62
|
builtIn: true,
|
|
63
63
|
createdBy: "system",
|
|
@@ -67,7 +67,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
67
67
|
name: "Provider Child Agent",
|
|
68
68
|
description: "Delegated child-agent runtime access, constrained to its parent and spawn request.",
|
|
69
69
|
role: "provider",
|
|
70
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "insights:write"],
|
|
70
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "insights:write"],
|
|
71
71
|
constraints: { canDelegate: false },
|
|
72
72
|
ttlSeconds: 2 * 60 * 60,
|
|
73
73
|
builtIn: true,
|
|
@@ -78,7 +78,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
78
78
|
name: "Provider Interactive Agent",
|
|
79
79
|
description: "User-launched provider runtime access constrained to its own agent and cwd for long interactive sessions.",
|
|
80
80
|
role: "provider",
|
|
81
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "insights:write"],
|
|
81
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "insights:write"],
|
|
82
82
|
constraints: { terminalAttach: false, logsRead: false, canDelegate: false },
|
|
83
83
|
ttlSeconds: 30 * 24 * 60 * 60,
|
|
84
84
|
builtIn: true,
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{Cn as e,Pt as t,Sn as n,jn as r,q as i,xn as a}from"./lucide-react-ChOGD4vc.js";import{a as o,t as s}from"./store-CtKmqMfx.js";import{G as c,H as l,W as u,c as d,s as f}from"./display-ConJ9cJB.js";import{t as p}from"./badge-CFVr4fG9.js";import{t as m}from"./button-BuTu0MHx.js";import{i as h,n as g,r as _,t as v}from"./card-C3ErQ372.js";var y=r();function b({direction:t}){return t===`inbound`?(0,y.jsx)(n,{className:`w-3 h-3`}):t===`outbound`?(0,y.jsx)(a,{className:`w-3 h-3`}):(0,y.jsx)(e,{className:`w-3 h-3`})}function x({channel:e}){let n=o(),r=s(e=>e.openChannelDetail),a=d(e),f=e.status!==`offline`;return(0,y.jsxs)(v,{className:`${f?``:`opacity-60`} hover:border-zinc-600 transition-colors`,children:[(0,y.jsx)(_,{className:`pb-2 pt-4 px-4`,children:(0,y.jsxs)(`div`,{className:`flex items-start gap-2`,children:[(0,y.jsx)(`div`,{className:`p-1.5 rounded-md ${f?`bg-blue-500/10`:`bg-zinc-800`}`,children:(0,y.jsx)(i,{className:`w-4 h-4 ${f?`text-blue-400`:`text-zinc-500`}`})}),(0,y.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,y.jsx)(h,{className:`text-sm font-medium truncate`,children:e.name||e.id}),(0,y.jsxs)(`div`,{className:`text-xs text-muted-foreground mt-0.5`,children:[e.type,` · `,e.transport]})]}),(0,y.jsx)(p,{className:`text-[10px] shrink-0 ${u(a.tone)}`,children:a.label})]})}),(0,y.jsxs)(g,{className:`px-4 pb-4 space-y-2.5`,children:[(0,y.jsxs)(`div`,{className:`flex items-center gap-1.5 text-xs text-muted-foreground`,children:[(0,y.jsx)(b,{direction:e.direction}),(0,y.jsx)(`span`,{className:`capitalize`,children:e.direction}),e.target&&(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(`span`,{children:`·`}),(0,y.jsx)(`span`,{className:`truncate text-foreground`,children:e.target})]})]}),e.targetHealth&&e.targetHealth.status!==`ok`&&(0,y.jsx)(`div`,{className:`text-xs px-2 py-1 rounded ${e.targetHealth.status===`error`?`bg-red-500/10 text-red-400`:`bg-yellow-500/10 text-yellow-400`}`,children:e.targetHealth.detail}),c(e.capabilities).length>0&&(0,y.jsx)(`div`,{className:`flex flex-wrap gap-1`,children:c(e.capabilities).map(e=>(0,y.jsx)(p,{variant:`secondary`,className:`text-[9px] px-1 py-0 h-4`,children:e},e))}),e.tags.length>0&&(0,y.jsx)(`div`,{className:`flex flex-wrap gap-1`,children:e.tags.map(e=>(0,y.jsx)(p,{variant:`outline`,className:`text-[9px] px-1 py-0 h-4`,children:e},e))}),(0,y.jsxs)(`div`,{className:`flex items-center justify-between pt-0.5`,children:[(0,y.jsx)(`span`,{className:`text-xs text-muted-foreground`,children:l(n,e.lastSeen)}),(0,y.jsxs)(m,{size:`sm`,variant:`outline`,className:`h-7 text-xs gap-1`,onClick:()=>r(e),children:[(0,y.jsx)(t,{className:`w-3 h-3`}),`Details`]})]})]})]})}function S(){let e=s(e=>e.channels),t=e.filter(f).length,n=e.filter(e=>e.targetHealth?.status===`error`).length,r=e.filter(e=>e.targetHealth?.status===`warning`).length;return(0,y.jsxs)(`div`,{className:`space-y-4`,children:[(0,y.jsxs)(`div`,{className:`flex items-center gap-3`,children:[(0,y.jsxs)(`div`,{children:[(0,y.jsx)(`h2`,{className:`text-base font-semibold`,children:`Channels`}),(0,y.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`Inbound / outbound message integrations`})]}),(0,y.jsxs)(`div`,{className:`flex items-center gap-2 ml-auto`,children:[t>0&&(0,y.jsxs)(p,{className:`text-xs bg-emerald-500/15 text-emerald-400 border-emerald-500/30`,children:[t,` ready`]}),r>0&&(0,y.jsxs)(p,{className:`text-xs bg-yellow-500/15 text-yellow-400 border-yellow-500/30`,children:[r,` warning`]}),n>0&&(0,y.jsxs)(p,{className:`text-xs bg-red-500/15 text-red-400 border-red-500/30`,children:[n,` error`]})]})]}),e.length===0?(0,y.jsx)(`div`,{className:`text-center py-16 text-muted-foreground text-sm`,children:`No channels registered.`}):(0,y.jsx)(`div`,{className:`grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4`,children:e.map(e=>(0,y.jsx)(x,{channel:e},e.id))})]})}export{S as ChannelsView};
|
|
2
|
-
//# sourceMappingURL=channels-CQy-JL7o.js.map
|