@openscout/protocol 0.2.109 → 0.2.110
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/dist/actors.d.ts +1 -1
- package/dist/actors.d.ts.map +1 -1
- package/dist/agent-notifications.d.ts +137 -0
- package/dist/agent-notifications.d.ts.map +1 -0
- package/dist/agent-notifications.js +148 -0
- package/dist/channel-invites.d.ts +3 -1
- package/dist/channel-invites.d.ts.map +1 -1
- package/dist/channel-membership.d.ts +10 -0
- package/dist/channel-membership.d.ts.map +1 -0
- package/dist/channel-membership.js +16 -0
- package/dist/chat-mentions.d.ts +4 -0
- package/dist/chat-mentions.d.ts.map +1 -0
- package/dist/chat-mentions.js +11 -0
- package/dist/chat-message-corrections.d.ts +28 -0
- package/dist/chat-message-corrections.d.ts.map +1 -0
- package/dist/chat-message-corrections.js +46 -0
- package/dist/chat-pins.d.ts +14 -0
- package/dist/chat-pins.d.ts.map +1 -0
- package/dist/chat-pins.js +37 -0
- package/dist/common.d.ts +1 -1
- package/dist/common.d.ts.map +1 -1
- package/dist/events.d.ts +4 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/herdr-workspace-digest.d.ts +8 -4
- package/dist/herdr-workspace-digest.d.ts.map +1 -1
- package/dist/herdr-workspace-digest.js +18 -3
- package/dist/host-integrations.js +6 -6
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/invocations.d.ts +17 -2
- package/dist/invocations.d.ts.map +1 -1
- package/dist/invocations.js +1 -1
- package/dist/mcp-core-tools.d.ts +137 -0
- package/dist/mcp-core-tools.d.ts.map +1 -0
- package/dist/mcp-core-tools.js +27 -0
- package/dist/message-reactions.d.ts +5 -0
- package/dist/message-reactions.d.ts.map +1 -1
- package/dist/message-reactions.js +10 -1
- package/dist/orchestration-provider-map.d.ts.map +1 -1
- package/dist/orchestration-provider-map.js +9 -1
- package/dist/read-receipts.d.ts +38 -0
- package/dist/read-receipts.d.ts.map +1 -1
- package/dist/read-receipts.js +43 -1
- package/dist/runtime-catalog-contract.d.ts +14 -0
- package/dist/runtime-catalog-contract.d.ts.map +1 -1
- package/dist/runtime-catalog.d.ts.map +1 -1
- package/dist/runtime-catalog.generated.d.ts +88 -2
- package/dist/runtime-catalog.generated.d.ts.map +1 -1
- package/dist/runtime-catalog.generated.js +114 -12
- package/dist/runtime-catalog.js +28 -0
- package/dist/runtime-execution.d.ts +33 -2
- package/dist/runtime-execution.d.ts.map +1 -1
- package/dist/runtime-execution.js +34 -17
- package/dist/scout-agent-card.d.ts +2 -0
- package/dist/scout-agent-card.d.ts.map +1 -1
- package/dist/scout-agent-card.js +3 -0
- package/dist/scout-composer.d.ts.map +1 -1
- package/dist/scout-composer.js +23 -0
- package/dist/scout-delivery.d.ts +3 -1
- package/dist/scout-delivery.d.ts.map +1 -1
- package/dist/scout-dispatch.d.ts +13 -0
- package/dist/scout-dispatch.d.ts.map +1 -1
- package/dist/session-handle.d.ts +37 -0
- package/dist/session-handle.d.ts.map +1 -1
- package/dist/session-handle.js +58 -0
- package/dist/terminal-host-topology.d.ts +5 -2
- package/dist/terminal-host-topology.d.ts.map +1 -1
- package/dist/terminal-host-topology.js +2 -1
- package/dist/transports.d.ts +8 -1
- package/dist/transports.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/actors.ts +2 -0
- package/src/agent-notifications.ts +241 -0
- package/src/channel-invites.ts +4 -1
- package/src/channel-membership.ts +22 -0
- package/src/chat-mentions.ts +11 -0
- package/src/chat-message-corrections.ts +40 -0
- package/src/chat-pins.ts +40 -0
- package/src/common.ts +2 -0
- package/src/events.ts +3 -0
- package/src/herdr-workspace-digest.ts +23 -6
- package/src/host-integrations.ts +6 -6
- package/src/index.ts +10 -0
- package/src/invocations.ts +18 -3
- package/src/mcp-core-tools.ts +46 -0
- package/src/message-reactions.ts +16 -2
- package/src/orchestration-provider-map.ts +12 -1
- package/src/read-receipts.ts +71 -0
- package/src/runtime-catalog-contract.ts +16 -0
- package/src/runtime-catalog.generated.ts +114 -12
- package/src/runtime-catalog.ts +32 -0
- package/src/runtime-catalog.v1.json +20 -13
- package/src/runtime-execution.ts +70 -16
- package/src/scout-agent-card.ts +5 -0
- package/src/scout-composer.ts +26 -0
- package/src/scout-delivery.ts +3 -1
- package/src/scout-dispatch.ts +13 -0
- package/src/session-handle.ts +88 -0
- package/src/terminal-host-topology.ts +5 -2
- package/src/transports.ts +9 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent notifications: what an agent's notification IS, so the phone can pick
|
|
3
|
+
* the view that fits it.
|
|
4
|
+
*
|
|
5
|
+
* Every notification declares a `view` and the typed fields that view needs.
|
|
6
|
+
* The same record renders in three places: the lock screen (text only, via
|
|
7
|
+
* {@link collapseAgentNotification}), the long-press view, and the app's
|
|
8
|
+
* Notifications row. A client that doesn't know a `view` renders `text`.
|
|
9
|
+
*
|
|
10
|
+
* Everything that wants the operator is a question of some kind; the prefix
|
|
11
|
+
* says WHEN it was asked:
|
|
12
|
+
* - `turn.*` is mid-turn: the turn is paused until the operator answers.
|
|
13
|
+
* - `ask` is outside a turn: the agent finished and reached out on its own.
|
|
14
|
+
* Only declared things become notifications. Inferred attention never does.
|
|
15
|
+
*
|
|
16
|
+
* The record travels sealed (see the runtime's mobile-push sealing): the relay
|
|
17
|
+
* that delivers the push never reads any of it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const AGENT_NOTIFICATION_VERSION = 1 as const;
|
|
21
|
+
|
|
22
|
+
export type AgentNotificationRisk = "low" | "medium" | "high";
|
|
23
|
+
|
|
24
|
+
export type AgentNotificationFileChange = {
|
|
25
|
+
path: string;
|
|
26
|
+
added: number;
|
|
27
|
+
removed: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type AgentNotificationArtifact = {
|
|
31
|
+
kind: "pr" | "build" | "file" | "link";
|
|
32
|
+
label: string;
|
|
33
|
+
meta?: string;
|
|
34
|
+
url?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type AgentNotificationView =
|
|
38
|
+
| { view: "turn.approve.command"; command: string; cwd?: string; risk: AgentNotificationRisk; why?: string }
|
|
39
|
+
| { view: "turn.approve.edit"; summary?: string; files: AgentNotificationFileChange[]; risk: AgentNotificationRisk }
|
|
40
|
+
| { view: "turn.approve.tool"; tool: string; summary?: string; risk: AgentNotificationRisk }
|
|
41
|
+
| { view: "turn.question.choice"; question: string; context?: string; options: string[]; multiSelect?: boolean }
|
|
42
|
+
| { view: "turn.question.open"; question: string; context?: string }
|
|
43
|
+
| { view: "turn.failed"; error: string; excerpt?: string[]; elapsed?: string }
|
|
44
|
+
| { view: "work.done"; summary: string; artifacts?: AgentNotificationArtifact[] }
|
|
45
|
+
| { view: "ask"; note: string; options?: string[] }
|
|
46
|
+
| { view: "text"; title: string; body?: string };
|
|
47
|
+
|
|
48
|
+
export type AgentNotificationViewName = AgentNotificationView["view"];
|
|
49
|
+
|
|
50
|
+
export type AgentNotificationAction =
|
|
51
|
+
| "approve"
|
|
52
|
+
| "deny"
|
|
53
|
+
| "reply"
|
|
54
|
+
| "open_chat"
|
|
55
|
+
| "open_link";
|
|
56
|
+
|
|
57
|
+
export type AgentNotification = AgentNotificationView & {
|
|
58
|
+
v: typeof AGENT_NOTIFICATION_VERSION;
|
|
59
|
+
/** Stable id of the thing that wants attention (inbox item, message). */
|
|
60
|
+
itemId: string;
|
|
61
|
+
/** The agent, as the operator knows it. */
|
|
62
|
+
sender: { name: string; agentId?: string };
|
|
63
|
+
project?: string;
|
|
64
|
+
/** The Mac it happened on. */
|
|
65
|
+
host?: string;
|
|
66
|
+
/** Declared and still open: Time Sensitive on the lock screen, Priority in the app. */
|
|
67
|
+
urgent: boolean;
|
|
68
|
+
createdAt: number;
|
|
69
|
+
/** Routing back into the app. */
|
|
70
|
+
route?: {
|
|
71
|
+
sessionId?: string;
|
|
72
|
+
turnId?: string;
|
|
73
|
+
blockId?: string;
|
|
74
|
+
conversationId?: string;
|
|
75
|
+
messageId?: string;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type AgentNotificationCollapse = {
|
|
80
|
+
/** Lock-screen title: the agent. */
|
|
81
|
+
title: string;
|
|
82
|
+
/** Lock-screen subtitle: project · host. */
|
|
83
|
+
subtitle: string;
|
|
84
|
+
/** Lock-screen body: headline, then the most useful line that fits as text. */
|
|
85
|
+
body: string;
|
|
86
|
+
/** Row headline in the app. Same words as line 1 of the body. */
|
|
87
|
+
headline: string;
|
|
88
|
+
/** iOS notification category, which carries the actions. */
|
|
89
|
+
category: string;
|
|
90
|
+
/** One thread per agent, so stacks group by who sent them. */
|
|
91
|
+
threadId: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* What travels sealed in a push: the record plus its lock-screen text,
|
|
96
|
+
* computed once here so the phone's extension applies it verbatim and no
|
|
97
|
+
* client re-derives the words.
|
|
98
|
+
*/
|
|
99
|
+
export type SealedAgentNotification = AgentNotification & {
|
|
100
|
+
lockScreen: Pick<AgentNotificationCollapse, "title" | "subtitle" | "body" | "category" | "threadId">;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export function sealedAgentNotification(n: AgentNotification): SealedAgentNotification {
|
|
104
|
+
const { title, subtitle, body, category, threadId } = collapseAgentNotification(n);
|
|
105
|
+
return { ...n, lockScreen: { title, subtitle, body, category, threadId } };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function riskLabel(risk: AgentNotificationRisk): string {
|
|
109
|
+
return risk === "high" ? "High risk" : risk === "medium" ? "Medium risk" : "Low risk";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function plural(count: number, one: string, many: string): string {
|
|
113
|
+
return `${count} ${count === 1 ? one : many}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function agentNotificationHeadline(n: AgentNotification): string {
|
|
117
|
+
switch (n.view) {
|
|
118
|
+
case "turn.approve.command": return "Wants to run a command";
|
|
119
|
+
case "turn.approve.edit": return `Wants to edit ${plural(n.files.length, "file", "files")}`;
|
|
120
|
+
case "turn.approve.tool": return `Wants to use ${n.tool}`;
|
|
121
|
+
case "turn.question.choice": return n.question;
|
|
122
|
+
case "turn.question.open": return n.question;
|
|
123
|
+
case "turn.failed": return `Turn failed: ${n.error}`;
|
|
124
|
+
case "work.done": return `Done: ${n.summary}`;
|
|
125
|
+
case "ask": return "Asked for you";
|
|
126
|
+
case "text": return n.title;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function detailLine(n: AgentNotification): string {
|
|
131
|
+
switch (n.view) {
|
|
132
|
+
case "turn.approve.command": return `${n.command} · ${riskLabel(n.risk)}`;
|
|
133
|
+
case "turn.approve.edit": {
|
|
134
|
+
const added = n.files.reduce((sum, f) => sum + f.added, 0);
|
|
135
|
+
const removed = n.files.reduce((sum, f) => sum + f.removed, 0);
|
|
136
|
+
const counts = `+${added} −${removed}`;
|
|
137
|
+
return n.summary ? `${counts} · ${n.summary}` : `${counts} · ${n.files.map((f) => f.path).join(", ")}`;
|
|
138
|
+
}
|
|
139
|
+
case "turn.approve.tool": return n.summary ? `${n.summary} · ${riskLabel(n.risk)}` : riskLabel(n.risk);
|
|
140
|
+
case "turn.question.choice": return n.options.join(" · ");
|
|
141
|
+
case "turn.question.open": return n.context ?? "";
|
|
142
|
+
case "turn.failed": return n.elapsed ? `After ${n.elapsed}. The session is still open.` : "The session is still open.";
|
|
143
|
+
case "work.done": return (n.artifacts ?? []).map((a) => a.meta ? `${a.label} ${a.meta}` : a.label).join(" · ");
|
|
144
|
+
case "ask": return n.note;
|
|
145
|
+
case "text": return n.body ?? "";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The actions a view offers, in order. The first is the primary one. */
|
|
150
|
+
export function agentNotificationActions(n: AgentNotification): AgentNotificationAction[] {
|
|
151
|
+
switch (n.view) {
|
|
152
|
+
case "turn.approve.command":
|
|
153
|
+
case "turn.approve.edit":
|
|
154
|
+
case "turn.approve.tool":
|
|
155
|
+
return ["approve", "deny"];
|
|
156
|
+
case "turn.question.choice":
|
|
157
|
+
case "turn.question.open":
|
|
158
|
+
case "ask":
|
|
159
|
+
return ["reply"];
|
|
160
|
+
case "turn.failed":
|
|
161
|
+
return ["open_chat"];
|
|
162
|
+
case "work.done":
|
|
163
|
+
return (n.artifacts ?? []).some((a) => a.url) ? ["open_link", "open_chat"] : ["open_chat"];
|
|
164
|
+
case "text":
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** iOS category ids. Registered by the app with their actions. */
|
|
170
|
+
export function agentNotificationCategory(n: AgentNotification): string {
|
|
171
|
+
switch (n.view) {
|
|
172
|
+
case "turn.approve.command":
|
|
173
|
+
case "turn.approve.edit":
|
|
174
|
+
case "turn.approve.tool":
|
|
175
|
+
return "scout.approval";
|
|
176
|
+
case "turn.question.choice":
|
|
177
|
+
case "turn.question.open":
|
|
178
|
+
case "ask":
|
|
179
|
+
return "scout.question";
|
|
180
|
+
case "turn.failed":
|
|
181
|
+
case "work.done":
|
|
182
|
+
return "scout.open";
|
|
183
|
+
case "text":
|
|
184
|
+
return "scout.text";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function collapseAgentNotification(n: AgentNotification): AgentNotificationCollapse {
|
|
189
|
+
const headline = agentNotificationHeadline(n);
|
|
190
|
+
const line2 = detailLine(n).trim();
|
|
191
|
+
const where = [n.project, n.host].filter((part): part is string => Boolean(part && part.trim())).join(" · ");
|
|
192
|
+
return {
|
|
193
|
+
title: n.sender.name,
|
|
194
|
+
subtitle: where,
|
|
195
|
+
body: line2 && line2 !== headline ? `${headline}\n${line2}` : headline,
|
|
196
|
+
headline,
|
|
197
|
+
category: agentNotificationCategory(n),
|
|
198
|
+
threadId: `scout.agent.${n.sender.agentId ?? n.sender.name}`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const MAX_TEXT = 600;
|
|
203
|
+
|
|
204
|
+
function clip(value: string, max = MAX_TEXT): string {
|
|
205
|
+
const trimmed = value.trim();
|
|
206
|
+
return trimmed.length <= max ? trimmed : `${trimmed.slice(0, max - 1)}…`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Bound every free-text field so a sealed record fits inside one APNs push
|
|
211
|
+
* (4 KB, shared with the alert text). Lists are capped too; the app loads the
|
|
212
|
+
* full item when it opens.
|
|
213
|
+
*/
|
|
214
|
+
export function clampAgentNotification(n: AgentNotification): AgentNotification {
|
|
215
|
+
const base = {
|
|
216
|
+
...n,
|
|
217
|
+
sender: { ...n.sender, name: clip(n.sender.name, 80) },
|
|
218
|
+
...(n.project ? { project: clip(n.project, 80) } : {}),
|
|
219
|
+
...(n.host ? { host: clip(n.host, 80) } : {}),
|
|
220
|
+
};
|
|
221
|
+
switch (base.view) {
|
|
222
|
+
case "turn.approve.command":
|
|
223
|
+
return { ...base, command: clip(base.command, 400), ...(base.why ? { why: clip(base.why, 300) } : {}) };
|
|
224
|
+
case "turn.approve.edit":
|
|
225
|
+
return { ...base, files: base.files.slice(0, 8).map((f) => ({ ...f, path: clip(f.path, 160) })), ...(base.summary ? { summary: clip(base.summary, 300) } : {}) };
|
|
226
|
+
case "turn.approve.tool":
|
|
227
|
+
return { ...base, tool: clip(base.tool, 80), ...(base.summary ? { summary: clip(base.summary, 300) } : {}) };
|
|
228
|
+
case "turn.question.choice":
|
|
229
|
+
return { ...base, question: clip(base.question, 400), options: base.options.slice(0, 6).map((o) => clip(o, 60)), ...(base.context ? { context: clip(base.context, 300) } : {}) };
|
|
230
|
+
case "turn.question.open":
|
|
231
|
+
return { ...base, question: clip(base.question, 400), ...(base.context ? { context: clip(base.context, 300) } : {}) };
|
|
232
|
+
case "turn.failed":
|
|
233
|
+
return { ...base, error: clip(base.error, 200), ...(base.excerpt ? { excerpt: base.excerpt.slice(-4).map((l) => clip(l, 120)) } : {}) };
|
|
234
|
+
case "work.done":
|
|
235
|
+
return { ...base, summary: clip(base.summary, 300), ...(base.artifacts ? { artifacts: base.artifacts.slice(0, 4) } : {}) };
|
|
236
|
+
case "ask":
|
|
237
|
+
return { ...base, note: clip(base.note, 500), ...(base.options ? { options: base.options.slice(0, 6).map((o) => clip(o, 60)) } : {}) };
|
|
238
|
+
case "text":
|
|
239
|
+
return { ...base, title: clip(base.title, 120), ...(base.body ? { body: clip(base.body, 400) } : {}) };
|
|
240
|
+
}
|
|
241
|
+
}
|
package/src/channel-invites.ts
CHANGED
|
@@ -126,7 +126,8 @@ export type ChannelInviteRejectionReason =
|
|
|
126
126
|
| "exhausted"
|
|
127
127
|
| "scope_mismatch"
|
|
128
128
|
| "channel_mismatch"
|
|
129
|
-
| "missing_identity"
|
|
129
|
+
| "missing_identity"
|
|
130
|
+
| "membership_removed";
|
|
130
131
|
|
|
131
132
|
export interface ChannelInviteRejection {
|
|
132
133
|
reason: ChannelInviteRejectionReason;
|
|
@@ -305,6 +306,8 @@ export function findChannelInviteByTokenHash(
|
|
|
305
306
|
* understand the offer, and nothing that could be replayed into membership.
|
|
306
307
|
*/
|
|
307
308
|
export interface ChannelInvitePublicView {
|
|
309
|
+
/** Explicit where the issuer supports single-use human invitations. */
|
|
310
|
+
kind?: "teammate" | "agent" | "api";
|
|
308
311
|
id: ScoutId;
|
|
309
312
|
channelId: ScoutId;
|
|
310
313
|
/** Public authorship, used to show who may revoke this invitation. */
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MetadataMap } from "./common.js";
|
|
2
|
+
|
|
3
|
+
/** Broker-written removals preserve history while invalidating older join links. */
|
|
4
|
+
export const CHANNEL_MEMBER_REMOVALS_KEY = "channelMemberRemovals";
|
|
5
|
+
|
|
6
|
+
export interface ChannelMemberRemoval {
|
|
7
|
+
removedAt: number;
|
|
8
|
+
removedByActorId: string;
|
|
9
|
+
blockedInviteIds: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readChannelMemberRemoval(metadata: MetadataMap | undefined, actorId: string): ChannelMemberRemoval | null {
|
|
13
|
+
const records = metadata?.[CHANNEL_MEMBER_REMOVALS_KEY];
|
|
14
|
+
if (!records || typeof records !== "object" || Array.isArray(records) || !Object.hasOwn(records, actorId)) return null;
|
|
15
|
+
const value = (records as Record<string, unknown>)[actorId];
|
|
16
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
17
|
+
const entry = value as Record<string, unknown>;
|
|
18
|
+
if (typeof entry.removedAt !== "number" || !Number.isFinite(entry.removedAt)
|
|
19
|
+
|| typeof entry.removedByActorId !== "string" || !Array.isArray(entry.blockedInviteIds)
|
|
20
|
+
|| !entry.blockedInviteIds.every(id => typeof id === "string")) return null;
|
|
21
|
+
return { removedAt: entry.removedAt, removedByActorId: entry.removedByActorId, blockedInviteIds: entry.blockedInviteIds as string[] };
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Explicit attention recipients. Text is payload and is never parsed as routing. */
|
|
2
|
+
export const MAX_CHAT_MENTIONS = 20;
|
|
3
|
+
|
|
4
|
+
export function parseChatMentionActorIds(value: unknown): string[] {
|
|
5
|
+
if (value === undefined) return [];
|
|
6
|
+
if (!Array.isArray(value) || value.length > MAX_CHAT_MENTIONS
|
|
7
|
+
|| value.some(id => typeof id !== "string" || !id.trim() || id.length > 256)) {
|
|
8
|
+
throw new Error("Choose at most 20 valid mention recipients.");
|
|
9
|
+
}
|
|
10
|
+
return [...new Set(value.map(id => (id as string).trim()))].sort();
|
|
11
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { MessageRecord } from "./messages.js";
|
|
2
|
+
|
|
3
|
+
export interface ChatMessageCorrection { revision: number; editedAt?: number; deletedAt?: number; changedBy: string }
|
|
4
|
+
export type ChatMessageChange = { expectedRevision: number; body: string } | { expectedRevision: number; deleted: true };
|
|
5
|
+
export class ChatMessageCorrectionError extends Error {
|
|
6
|
+
constructor(public readonly status: 400 | 403 | 404 | 409, message: string) { super(message); }
|
|
7
|
+
}
|
|
8
|
+
export function readChatMessageCorrection(metadata: unknown): ChatMessageCorrection | undefined {
|
|
9
|
+
const value = (metadata as { chatCorrection?: unknown } | null)?.chatCorrection;
|
|
10
|
+
if (!value || typeof value !== "object") return;
|
|
11
|
+
const item = value as ChatMessageCorrection;
|
|
12
|
+
if (!Number.isSafeInteger(item.revision) || item.revision < 1 || typeof item.changedBy !== "string") return;
|
|
13
|
+
return { revision: item.revision, changedBy: item.changedBy,
|
|
14
|
+
...(typeof item.editedAt === "number" && Number.isFinite(item.editedAt) ? { editedAt: item.editedAt } : {}),
|
|
15
|
+
...(typeof item.deletedAt === "number" && Number.isFinite(item.deletedAt) ? { deletedAt: item.deletedAt } : {}) };
|
|
16
|
+
}
|
|
17
|
+
export function parseChatMessageChange(value: unknown): ChatMessageChange {
|
|
18
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new ChatMessageCorrectionError(400, "Invalid message change.");
|
|
19
|
+
const input = value as Record<string, unknown>;
|
|
20
|
+
if (Object.keys(input).length !== 2 || !Number.isSafeInteger(input.expectedRevision) || (input.expectedRevision as number) < 0) throw new ChatMessageCorrectionError(400, "Invalid message revision.");
|
|
21
|
+
if (input.deleted === true) return { expectedRevision: input.expectedRevision as number, deleted: true };
|
|
22
|
+
if (typeof input.body !== "string" || !input.body.trim() || input.body.length > 32_000) throw new ChatMessageCorrectionError(400, "Message must contain between 1 and 32000 characters.");
|
|
23
|
+
return { expectedRevision: input.expectedRevision as number, body: input.body };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A correction affects the shared record, never a new delivery or invocation. */
|
|
27
|
+
export function correctChatMessage<T extends Pick<MessageRecord, "actorId" | "body"> & {
|
|
28
|
+
metadata?: MessageRecord["metadata"] | null; attachments?: unknown[]; mentions?: unknown[]; speech?: unknown;
|
|
29
|
+
}>(message: T, input: unknown, actorId: string, canModerate: boolean, now: number): T {
|
|
30
|
+
const change = parseChatMessageChange(input);
|
|
31
|
+
const prior = readChatMessageCorrection(message.metadata);
|
|
32
|
+
const deleting = "deleted" in change;
|
|
33
|
+
if (message.actorId !== actorId && !(deleting && canModerate)) throw new ChatMessageCorrectionError(403, "Only the author can edit this message. Hosts can also delete messages.");
|
|
34
|
+
if ((prior?.revision ?? 0) !== change.expectedRevision) throw new ChatMessageCorrectionError(409, "This message changed since you opened it. Review the latest version before trying again.");
|
|
35
|
+
if (prior?.deletedAt != null) throw new ChatMessageCorrectionError(409, "This message has already been deleted.");
|
|
36
|
+
const correction: ChatMessageCorrection = { ...prior, revision: (prior?.revision ?? 0) + 1, changedBy: actorId, ...(deleting ? { deletedAt: now } : { editedAt: now }) };
|
|
37
|
+
return { ...message, body: deleting ? "" : change.body,
|
|
38
|
+
...(deleting ? { attachments: [], mentions: [], speech: undefined } : {}),
|
|
39
|
+
metadata: { ...message.metadata, chatCorrection: correction } };
|
|
40
|
+
}
|
package/src/chat-pins.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Channel-visible references, distinct from a person's private saved messages. */
|
|
2
|
+
export interface ChatPin {
|
|
3
|
+
messageId: string;
|
|
4
|
+
pinnedBy: string;
|
|
5
|
+
pinnedAt: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ChatPinChange { messageId: string; pinned: boolean }
|
|
8
|
+
|
|
9
|
+
export function readChatPins(value: unknown): ChatPin[] {
|
|
10
|
+
if (!Array.isArray(value)) return [];
|
|
11
|
+
const seen = new Set<string>();
|
|
12
|
+
return value.filter((item): item is ChatPin => {
|
|
13
|
+
if (!item || typeof item !== "object" || typeof item.messageId !== "string" || !item.messageId
|
|
14
|
+
|| typeof item.pinnedBy !== "string" || !item.pinnedBy || typeof item.pinnedAt !== "number"
|
|
15
|
+
|| !Number.isFinite(item.pinnedAt) || seen.has(item.messageId)) return false;
|
|
16
|
+
seen.add(item.messageId);
|
|
17
|
+
return true;
|
|
18
|
+
}).map(({ messageId, pinnedBy, pinnedAt }) => ({ messageId, pinnedBy, pinnedAt }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseChatPinChange(value: unknown): ChatPinChange {
|
|
22
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid channel pin change.");
|
|
23
|
+
const input = value as Record<string, unknown>;
|
|
24
|
+
if (Object.keys(input).length !== 2 || typeof input.messageId !== "string" || !input.messageId
|
|
25
|
+
|| input.messageId.trim() !== input.messageId || input.messageId.length > 256 || typeof input.pinned !== "boolean") {
|
|
26
|
+
throw new Error("Invalid channel pin change.");
|
|
27
|
+
}
|
|
28
|
+
return { messageId: input.messageId, pinned: input.pinned };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function applyChatPinChange(current: unknown, change: unknown, actorId: string, now: number): ChatPin[] {
|
|
32
|
+
const input = parseChatPinChange(change);
|
|
33
|
+
const pins = readChatPins(current);
|
|
34
|
+
if (!input.pinned) return pins.filter(pin => pin.messageId !== input.messageId);
|
|
35
|
+
// An idempotent repeat must not rewrite who originally pinned the message.
|
|
36
|
+
if (pins.some(pin => pin.messageId === input.messageId)) return pins;
|
|
37
|
+
if (pins.length >= 50) throw new Error("Channel pin limit reached. Unpin a message before adding another.");
|
|
38
|
+
if (!actorId || !Number.isFinite(now)) throw new Error("Invalid pin attribution.");
|
|
39
|
+
return [...pins, { messageId: input.messageId, pinnedBy: actorId, pinnedAt: now }];
|
|
40
|
+
}
|
package/src/common.ts
CHANGED
package/src/events.ts
CHANGED
|
@@ -61,6 +61,8 @@ export type MessagePostedEvent = ControlEventBase<"message.posted", {
|
|
|
61
61
|
message: MessageRecord;
|
|
62
62
|
}>;
|
|
63
63
|
|
|
64
|
+
export type MessageCorrectedEvent = ControlEventBase<"message.corrected", { message: MessageRecord }>;
|
|
65
|
+
|
|
64
66
|
export type ConversationReadCursorUpdatedEvent = ControlEventBase<"conversation.read_cursor.updated", {
|
|
65
67
|
cursor: ConversationReadCursor;
|
|
66
68
|
}>;
|
|
@@ -122,6 +124,7 @@ export type ControlEvent =
|
|
|
122
124
|
| ConversationUpsertedEvent
|
|
123
125
|
| BindingUpsertedEvent
|
|
124
126
|
| MessagePostedEvent
|
|
127
|
+
| MessageCorrectedEvent
|
|
125
128
|
| ConversationReadCursorUpdatedEvent
|
|
126
129
|
| InvocationRequestedEvent
|
|
127
130
|
| FlightUpdatedEvent
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import type {
|
|
32
|
+
HerdrAgentSessionRef,
|
|
32
33
|
HerdrAgentStatus,
|
|
33
34
|
HerdrPaneProjection,
|
|
34
35
|
HerdrSessionTopology,
|
|
@@ -45,15 +46,19 @@ export type HerdrStatusCounts = Record<HerdrAgentStatus, number>;
|
|
|
45
46
|
|
|
46
47
|
export type HerdrDigestPane = {
|
|
47
48
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
49
|
+
* Observed host locator: terminal id when reported, else pane id. This is
|
|
50
|
+
* not a Scout address or a guaranteed CLI target (Herdr versions differ).
|
|
51
|
+
* Read handoffs use the host session + pane id. Null on a saved projection.
|
|
51
52
|
*/
|
|
52
53
|
target: string | null;
|
|
53
54
|
paneId: string;
|
|
54
55
|
workspaceId: string;
|
|
55
56
|
tabId: string;
|
|
57
|
+
/** Operator-assigned pane name, not a Scout agent address. */
|
|
58
|
+
name?: string | null;
|
|
56
59
|
label: string | null;
|
|
60
|
+
/** Herdr-reported identity, not a verified Scout session binding. */
|
|
61
|
+
agentSession?: HerdrAgentSessionRef | null;
|
|
57
62
|
/** Detected agent label ("claude", "codex"), or null for a plain shell. */
|
|
58
63
|
agent: string | null;
|
|
59
64
|
status: HerdrAgentStatus;
|
|
@@ -168,7 +173,9 @@ function toDigestPane(pane: HerdrPaneProjection): HerdrDigestPane {
|
|
|
168
173
|
paneId: pane.paneId,
|
|
169
174
|
workspaceId: pane.workspaceId,
|
|
170
175
|
tabId: pane.tabId,
|
|
176
|
+
name: pane.name ?? null,
|
|
171
177
|
label: pane.label,
|
|
178
|
+
agentSession: pane.agentSession,
|
|
172
179
|
agent: pane.agent,
|
|
173
180
|
status: pane.agentStatus,
|
|
174
181
|
directory: paneDirectory(pane),
|
|
@@ -262,6 +269,10 @@ export function digestHerdrTopology(
|
|
|
262
269
|
});
|
|
263
270
|
for (const pane of tab.panes) {
|
|
264
271
|
const digestPane = toDigestPane(pane);
|
|
272
|
+
if (!topology.running) {
|
|
273
|
+
digestPane.target = null;
|
|
274
|
+
digestPane.status = "unknown";
|
|
275
|
+
}
|
|
265
276
|
counts[digestPane.status] += 1;
|
|
266
277
|
if (digestPane.agent) agentTotal += 1;
|
|
267
278
|
all.push(digestPane);
|
|
@@ -372,10 +383,14 @@ export function digestHerdrTopology(
|
|
|
372
383
|
* every row buys nothing.
|
|
373
384
|
*/
|
|
374
385
|
function paneLine(pane: HerdrDigestPane): string {
|
|
375
|
-
const
|
|
376
|
-
|
|
386
|
+
const title = pane.name ?? pane.label;
|
|
387
|
+
const parts = title
|
|
388
|
+
? [title, pane.agent ?? "shell", pane.paneId]
|
|
377
389
|
: [pane.paneId, pane.agent ?? "shell"];
|
|
390
|
+
if (pane.name && pane.label && pane.name !== pane.label) parts.push(pane.label);
|
|
378
391
|
if (pane.directory) parts.push(directoryName(pane.directory));
|
|
392
|
+
parts.push(pane.status);
|
|
393
|
+
if (pane.agentSession) parts.push(`reported ${pane.agentSession.agent} ${pane.agentSession.kind}:${pane.agentSession.value}`);
|
|
379
394
|
if (pane.focused) parts.push("focused");
|
|
380
395
|
return ` ${parts.join(" ")}`;
|
|
381
396
|
}
|
|
@@ -422,9 +437,10 @@ export function renderHerdrWorkspaceDigest(digest: HerdrWorkspaceDigest): string
|
|
|
422
437
|
// scanning reader never reaches a footnote, and these are the lines that stop
|
|
423
438
|
// someone believing `unknown` meant done or that a stale layout was current.
|
|
424
439
|
for (const note of digest.notes) lines.push(` ${note}`);
|
|
440
|
+
lines.push(` Observed ${new Date(digest.observedAt).toISOString()}; pane names are host labels, not Scout addresses.`);
|
|
425
441
|
|
|
426
442
|
if (digest.needsYou.length) {
|
|
427
|
-
lines.push("", "
|
|
443
|
+
lines.push("", "Requests");
|
|
428
444
|
for (const pane of digest.needsYou) lines.push(paneLine(pane));
|
|
429
445
|
}
|
|
430
446
|
if (digest.working.length) {
|
|
@@ -439,6 +455,7 @@ export function renderHerdrWorkspaceDigest(digest: HerdrWorkspaceDigest): string
|
|
|
439
455
|
const where = group.directory ? ` (${group.directory})` : "";
|
|
440
456
|
const hidden = omittedLabel(group.omittedCounts);
|
|
441
457
|
lines.push(` ${group.name}${where} — ${total} pane${total === 1 ? "" : "s"} · ${countsLine(group.counts)}${hidden ? ` · ${hidden}` : ""}`);
|
|
458
|
+
for (const pane of group.panes) lines.push(` ${paneLine(pane)}`);
|
|
442
459
|
}
|
|
443
460
|
}
|
|
444
461
|
|
package/src/host-integrations.ts
CHANGED
|
@@ -38,8 +38,8 @@ export const SCOUT_HOST_INTEGRATIONS: readonly ScoutHostIntegration[] = [
|
|
|
38
38
|
roles: ["execution_harness", "agent_host", "mcp_host"],
|
|
39
39
|
harness: "claude",
|
|
40
40
|
homepage: "https://claude.ai/claude-code",
|
|
41
|
-
repositoryUrl: "https://github.com/
|
|
42
|
-
installHint: "/plugin marketplace add
|
|
41
|
+
repositoryUrl: "https://github.com/oscout/claude-scout",
|
|
42
|
+
installHint: "/plugin marketplace add oscout/claude-scout",
|
|
43
43
|
summary: "Claude Code can run Scout-routed work as a harness and expose Scout through a host plugin.",
|
|
44
44
|
},
|
|
45
45
|
{
|
|
@@ -48,8 +48,8 @@ export const SCOUT_HOST_INTEGRATIONS: readonly ScoutHostIntegration[] = [
|
|
|
48
48
|
roles: ["execution_harness", "agent_host", "mcp_host"],
|
|
49
49
|
harness: "codex",
|
|
50
50
|
homepage: "https://github.com/openai/codex",
|
|
51
|
-
repositoryUrl: "https://github.com/
|
|
52
|
-
installHint: "/plugin marketplace add
|
|
51
|
+
repositoryUrl: "https://github.com/oscout/codex-scout",
|
|
52
|
+
installHint: "/plugin marketplace add oscout/codex-scout",
|
|
53
53
|
summary: "Codex can run Scout-routed work as a harness and expose Scout through MCP/plugin surfaces.",
|
|
54
54
|
},
|
|
55
55
|
{
|
|
@@ -57,8 +57,8 @@ export const SCOUT_HOST_INTEGRATIONS: readonly ScoutHostIntegration[] = [
|
|
|
57
57
|
label: "Cursor",
|
|
58
58
|
roles: ["execution_harness", "agent_host", "mcp_host"],
|
|
59
59
|
harness: "cursor",
|
|
60
|
-
repositoryUrl: "https://github.com/
|
|
61
|
-
installHint: "See https://github.com/
|
|
60
|
+
repositoryUrl: "https://github.com/oscout/cursor-scout",
|
|
61
|
+
installHint: "See https://github.com/oscout/cursor-scout for the host-specific installer.",
|
|
62
62
|
summary: "Cursor is an agent host and execution target when routed through its supported CLI or MCP surfaces.",
|
|
63
63
|
},
|
|
64
64
|
{
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./conversations.js";
|
|
|
12
12
|
export * from "./conversation-projection.js";
|
|
13
13
|
export * from "./channel-identity.js";
|
|
14
14
|
export * from "./channel-invites.js";
|
|
15
|
+
export * from "./channel-membership.js";
|
|
15
16
|
export * from "./channel-reception.js";
|
|
16
17
|
export * from "./collaboration.js";
|
|
17
18
|
export * from "./assigned-roles.js";
|
|
@@ -61,3 +62,12 @@ export * from "./terminal-host-topology.js";
|
|
|
61
62
|
export * from "./herdr-workspace-digest.js";
|
|
62
63
|
export * from "./broker-routes.js";
|
|
63
64
|
export * from "./rendezvous.js";
|
|
65
|
+
|
|
66
|
+
export * from "./chat-pins.js";
|
|
67
|
+
export * from "./chat-message-corrections.js";
|
|
68
|
+
|
|
69
|
+
export * from "./chat-mentions.js";
|
|
70
|
+
|
|
71
|
+
export * from "./agent-notifications.js";
|
|
72
|
+
|
|
73
|
+
export * from "./mcp-core-tools.js";
|
package/src/invocations.ts
CHANGED
|
@@ -27,8 +27,12 @@ export type InvocationSessionPolicy =
|
|
|
27
27
|
/** @deprecated use reuse */
|
|
28
28
|
| "any";
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Where a newly created harness session should be discoverable.
|
|
32
|
+
* `attached` (Codex only) runs the session on an already-running Codex
|
|
33
|
+
* app-server (the daemon control socket) instead of a Scout-spawned one.
|
|
34
|
+
*/
|
|
35
|
+
export type SessionPlacement = "background" | "foreground" | "attached";
|
|
32
36
|
|
|
33
37
|
export type InvocationForkSourceKind =
|
|
34
38
|
| "native_thread_clone"
|
|
@@ -72,6 +76,17 @@ export interface InvocationExecutionPreference {
|
|
|
72
76
|
forkFromSessionId?: ScoutId;
|
|
73
77
|
forkContext?: InvocationForkContextOptions;
|
|
74
78
|
lineage?: InvocationSessionLineage;
|
|
79
|
+
/**
|
|
80
|
+
* Hard per-turn execution budget in milliseconds, honoured only by the ACP
|
|
81
|
+
* wrappers (Grok, Kimi, OpenCode, Devin, Cursor). Tmux, Claude stream-JSON,
|
|
82
|
+
* Codex app-server, and Pi RPC do not implement a turn budget. Distinct
|
|
83
|
+
* from the requester's wait budget (`timeoutMs`): a caller willing to wait
|
|
84
|
+
* 24 h does not license a 24 h turn. When set it takes precedence over the
|
|
85
|
+
* OPENSCOUT_ACP_HARD_CEILING_MS default (typically to raise it); bounded to
|
|
86
|
+
* 6 h at the command boundary and always clamped to the administrator
|
|
87
|
+
* maximum OPENSCOUT_ACP_HARD_CEILING_MAX_MS (default 6 h).
|
|
88
|
+
*/
|
|
89
|
+
turnBudgetMs?: number;
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
export interface InvocationRequest {
|
|
@@ -158,7 +173,7 @@ function cleanTraceTimestamp(value: unknown): number | undefined {
|
|
|
158
173
|
}
|
|
159
174
|
|
|
160
175
|
function cleanTracePlacement(value: unknown): SessionPlacement | undefined {
|
|
161
|
-
return value === "background" || value === "foreground" ? value : undefined;
|
|
176
|
+
return value === "background" || value === "foreground" || value === "attached" ? value : undefined;
|
|
162
177
|
}
|
|
163
178
|
|
|
164
179
|
function parseFlightSessionTraceEntry(value: unknown): FlightSessionTraceEntry | null {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `mcp:core` tool tier: what the hosted MCP gateway (mcp.oscout.net)
|
|
3
|
+
* exposes to a remote client through the bridge. The bridge filters on these
|
|
4
|
+
* names, the gateway's agent guide counts them, and the public docs list them.
|
|
5
|
+
* Titles and access must match the registered tools; a desktop test enforces it.
|
|
6
|
+
*/
|
|
7
|
+
export type ScoutMcpCoreToolAccess = "read" | "write";
|
|
8
|
+
|
|
9
|
+
export type ScoutMcpCoreTool = {
|
|
10
|
+
name: string;
|
|
11
|
+
title: string;
|
|
12
|
+
access: ScoutMcpCoreToolAccess;
|
|
13
|
+
summary: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const SCOUT_MCP_CORE_TOOL_CATALOG = [
|
|
17
|
+
{ name: "whoami", title: "Scout Whoami", access: "read", summary: "Show the Scout identity and broker this connection acts as." },
|
|
18
|
+
{ name: "agents_search", title: "Search Scout Agents", access: "read", summary: "Find agents on the mesh that can take a request." },
|
|
19
|
+
{ name: "agents_resolve", title: "Resolve Scout Agent", access: "read", summary: "Resolve one agent handle, or report why it is ambiguous." },
|
|
20
|
+
{ name: "ask", title: "Ask", access: "write", summary: "Ask an agent to answer, review, or build something, and get a handle to follow." },
|
|
21
|
+
{ name: "invocations_get", title: "Get Scout Ask", access: "read", summary: "Read the current state of an ask." },
|
|
22
|
+
{ name: "invocations_wait", title: "Wait For Scout Ask", access: "read", summary: "Wait briefly for an ask to finish and return its state." },
|
|
23
|
+
{ name: "messages_send", title: "Send Scout Message", access: "write", summary: "Send a direct message or a channel post." },
|
|
24
|
+
{ name: "messages_reply", title: "Reply to Scout Message", access: "write", summary: "Reply inside an existing conversation." },
|
|
25
|
+
{ name: "messages_inbox", title: "Read Scout Inbox", access: "read", summary: "Read recent messages addressed to this identity." },
|
|
26
|
+
{ name: "messages_channel", title: "Read Scout Channel", access: "read", summary: "Read recent messages in a named channel." },
|
|
27
|
+
{ name: "current_reply_context", title: "Current Scout Reply Context", access: "read", summary: "Check whether a reply would continue an inbound ask." },
|
|
28
|
+
{ name: "broker_feed", title: "Read Agent Broker Feed", access: "read", summary: "Read one agent's messages, deliveries, and errors in one view." },
|
|
29
|
+
{ name: "tail_events", title: "Read Tail Events", access: "read", summary: "Read recent activity from the coding agents on the Scout machine." },
|
|
30
|
+
{ name: "labels_brief", title: "Brief Scout Label", access: "read", summary: "Summarize the records that share a label." },
|
|
31
|
+
{ name: "labels_feed", title: "Read Scout Label Feed", access: "read", summary: "Read the event backlog for a label." },
|
|
32
|
+
{ name: "work_update", title: "Update Scout Work", access: "write", summary: "Move a work item through progress, review, and done." },
|
|
33
|
+
{ name: "notify_operator", title: "Notify Operator", access: "write", summary: "Send the human operator a non-blocking note." },
|
|
34
|
+
{ name: "consult_operator", title: "Consult Operator Without Blocking", access: "write", summary: "Ask the operator for advice while continuing with a stated default." },
|
|
35
|
+
{ name: "feedback_send", title: "Send Scout Feedback", access: "write", summary: "Report a bug or rough edge in Scout to the OpenScout team." },
|
|
36
|
+
{ name: "sessions_attach", title: "Attach External Session", access: "write", summary: "Give this conversation a Scout mailbox so agents can reply to it." },
|
|
37
|
+
{ name: "sessions_get", title: "Get External Session", access: "read", summary: "Read the mailbox attached to this conversation." },
|
|
38
|
+
{ name: "sessions_poll", title: "Poll External Session", access: "read", summary: "Read pending items in the attached mailbox." },
|
|
39
|
+
{ name: "sessions_ack", title: "Acknowledge External Session Item", access: "write", summary: "Mark a mailbox item as received." },
|
|
40
|
+
{ name: "sessions_reply", title: "Reply to External Session Item", access: "write", summary: "Send the final answer for a delivered task." },
|
|
41
|
+
] as const satisfies readonly ScoutMcpCoreTool[];
|
|
42
|
+
|
|
43
|
+
export type ScoutMcpCoreToolName = (typeof SCOUT_MCP_CORE_TOOL_CATALOG)[number]["name"];
|
|
44
|
+
|
|
45
|
+
export const SCOUT_MCP_CORE_TOOL_NAMES: readonly ScoutMcpCoreToolName[] =
|
|
46
|
+
SCOUT_MCP_CORE_TOOL_CATALOG.map((tool) => tool.name);
|