pi-extensions-tool-display 0.1.1 → 0.2.1
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 +13 -3
- package/README.zh-CN.md +14 -4
- package/config.example.json +40 -0
- package/index.ts +6 -0
- package/package.json +16 -5
- package/src/agent-dir.ts +32 -0
- package/src/ansi-utils.ts +116 -0
- package/src/bash-display.ts +191 -0
- package/src/capabilities.ts +85 -0
- package/src/config-modal.ts +540 -0
- package/src/config-store.ts +303 -0
- package/src/debug-logger.ts +150 -0
- package/src/diff-presentation.ts +72 -0
- package/src/diff-renderer.ts +2517 -0
- package/src/disposable.ts +34 -0
- package/src/extension-lifecycle.ts +14 -0
- package/src/index.ts +138 -85
- package/src/line-width-safety.ts +93 -0
- package/src/modal-icons.ts +57 -0
- package/src/pending-diff-preview.ts +371 -0
- package/src/presets.ts +108 -0
- package/src/render-utils.ts +186 -0
- package/src/result-render-middleware.ts +98 -0
- package/src/settings-inspector-modal.ts +475 -0
- package/src/thinking-label.ts +343 -0
- package/src/tool-metadata.ts +139 -0
- package/src/tool-overrides.ts +2237 -0
- package/src/types.ts +105 -0
- package/src/user-message-box-markdown.ts +64 -0
- package/src/user-message-box-native.ts +66 -0
- package/src/user-message-box-patch.ts +72 -0
- package/src/user-message-box-renderer.ts +419 -0
- package/src/user-message-box-utils.ts +106 -0
- package/src/write-display-utils.ts +31 -0
- package/src/zellij-modal.ts +1027 -0
- package/tool-display-api-consumer.d.ts +68 -0
- package/tool-display-api-consumer.js +82 -0
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { isRecord } from "./tool-metadata.js";
|
|
3
|
+
import { onReloadShutdown } from "./extension-lifecycle.js";
|
|
4
|
+
|
|
5
|
+
interface ThemeLike {
|
|
6
|
+
fg(color: string, text: string): string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface AssistantMessageLike {
|
|
10
|
+
role?: unknown;
|
|
11
|
+
api?: unknown;
|
|
12
|
+
content?: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const THINKING_CHAT_PREFIX = "Thinking: ";
|
|
16
|
+
const THINKING_LABEL_PREFIX_PATTERN = /^(?:thinking:\s*)+/i;
|
|
17
|
+
const LEADING_ANSI_FRAGMENT_PATTERN = /^(?:\s*;?\d{1,3}(?:;\d{1,3})*m)+\s*/;
|
|
18
|
+
const MAX_THINKING_CONTENT_DEPTH = 16;
|
|
19
|
+
|
|
20
|
+
const registeredThinkingApis = new WeakSet<ExtensionAPI>();
|
|
21
|
+
|
|
22
|
+
const OPENAI_REASONING_APIS = new Set([
|
|
23
|
+
"openai-completions",
|
|
24
|
+
"openai-responses",
|
|
25
|
+
"openai-codex-responses",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const ANTHROPIC_REASONING_APIS = new Set([
|
|
29
|
+
"anthropic-messages",
|
|
30
|
+
"anthropic-responses",
|
|
31
|
+
"anthropic-completions",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function normalizeApiName(api: unknown): string | undefined {
|
|
35
|
+
if (typeof api !== "string") {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const normalized = api.trim().toLowerCase();
|
|
40
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shouldPrefixThinkingForApi(api: unknown): boolean {
|
|
44
|
+
const normalizedApi = normalizeApiName(api);
|
|
45
|
+
if (!normalizedApi) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (OPENAI_REASONING_APIS.has(normalizedApi)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (ANTHROPIC_REASONING_APIS.has(normalizedApi)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (normalizedApi.startsWith("anthropic-")) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Keep OpenAI handling explicit to avoid applying this formatter to
|
|
62
|
+
// unrelated OpenAI transport APIs that may not emit thinking blocks.
|
|
63
|
+
if (normalizedApi.startsWith("openai-")) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// For non-OpenAI providers, apply the prefix when thinking blocks exist.
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stripAnsi(text: string): string {
|
|
72
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function stripLeadingAnsiFragments(text: string): string {
|
|
76
|
+
let current = text;
|
|
77
|
+
while (true) {
|
|
78
|
+
const next = current.replace(LEADING_ANSI_FRAGMENT_PATTERN, "");
|
|
79
|
+
if (next === current) {
|
|
80
|
+
return current;
|
|
81
|
+
}
|
|
82
|
+
current = next;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stripThinkingPresentationArtifacts(text: string): string {
|
|
87
|
+
let current = stripAnsi(text);
|
|
88
|
+
let removedThinkingLabel = false;
|
|
89
|
+
|
|
90
|
+
while (true) {
|
|
91
|
+
const withoutLabel = current
|
|
92
|
+
.replace(THINKING_LABEL_PREFIX_PATTERN, "")
|
|
93
|
+
.trimStart();
|
|
94
|
+
if (withoutLabel !== current) {
|
|
95
|
+
current = withoutLabel;
|
|
96
|
+
removedThinkingLabel = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const withoutAnsiFragments = stripLeadingAnsiFragments(current).trimStart();
|
|
101
|
+
if (withoutAnsiFragments !== current) {
|
|
102
|
+
const fragmentExposedAnotherLabel =
|
|
103
|
+
withoutAnsiFragments.replace(THINKING_LABEL_PREFIX_PATTERN, "").trimStart() !==
|
|
104
|
+
withoutAnsiFragments;
|
|
105
|
+
|
|
106
|
+
if (removedThinkingLabel || fragmentExposedAnotherLabel) {
|
|
107
|
+
current = withoutAnsiFragments;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return current;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatThinkingLabel(theme: ThemeLike | undefined, thinkingText: string): string {
|
|
117
|
+
if (!theme) {
|
|
118
|
+
return `${THINKING_CHAT_PREFIX}${thinkingText}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const label = theme.fg("accent", THINKING_CHAT_PREFIX.trimEnd());
|
|
122
|
+
const body = theme.fg("thinkingText", thinkingText);
|
|
123
|
+
return `${label} ${body}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function prefixThinkingLine(text: string, theme: ThemeLike | undefined): string {
|
|
127
|
+
const normalizedThinking = stripThinkingPresentationArtifacts(text).trim();
|
|
128
|
+
if (!normalizedThinking) {
|
|
129
|
+
return text;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return formatThinkingLabel(theme, normalizedThinking);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeThinkingLineForContext(text: string): string {
|
|
136
|
+
return stripThinkingPresentationArtifacts(text);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isThinkingBlock(value: unknown): value is Record<string, unknown> & {
|
|
140
|
+
type: "thinking";
|
|
141
|
+
thinking: string;
|
|
142
|
+
} {
|
|
143
|
+
if (!isRecord(value)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return value.type === "thinking" && typeof value.thinking === "string";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function mapThinkingContentArray(
|
|
151
|
+
content: unknown[],
|
|
152
|
+
mapThinkingText: (text: string) => string,
|
|
153
|
+
depth = 0,
|
|
154
|
+
seen: WeakSet<object> = new WeakSet<object>(),
|
|
155
|
+
): { content: unknown[]; changed: boolean } {
|
|
156
|
+
if (depth > MAX_THINKING_CONTENT_DEPTH || seen.has(content)) {
|
|
157
|
+
return { content, changed: false };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
seen.add(content);
|
|
161
|
+
let changed = false;
|
|
162
|
+
const nextContent = content.map((block) => {
|
|
163
|
+
if (Array.isArray(block)) {
|
|
164
|
+
const nested = mapThinkingContentArray(
|
|
165
|
+
block,
|
|
166
|
+
mapThinkingText,
|
|
167
|
+
depth + 1,
|
|
168
|
+
seen,
|
|
169
|
+
);
|
|
170
|
+
if (nested.changed) {
|
|
171
|
+
changed = true;
|
|
172
|
+
return nested.content;
|
|
173
|
+
}
|
|
174
|
+
return block as unknown[];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!isThinkingBlock(block)) {
|
|
178
|
+
return block;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const nextThinking = mapThinkingText(block.thinking);
|
|
182
|
+
if (nextThinking === block.thinking) {
|
|
183
|
+
return block;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
changed = true;
|
|
187
|
+
return { ...block, thinking: nextThinking };
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return { content: changed ? nextContent : content, changed };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function withThinkingLabelsForDisplay(
|
|
194
|
+
content: unknown,
|
|
195
|
+
theme: ThemeLike | undefined,
|
|
196
|
+
): unknown {
|
|
197
|
+
if (!Array.isArray(content)) {
|
|
198
|
+
return content;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const mapped = mapThinkingContentArray(content, (thinking) =>
|
|
202
|
+
prefixThinkingLine(thinking, theme),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
return mapped.changed ? mapped.content : content;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function sanitizeThinkingBlocksForContext(message: AssistantMessageLike): AssistantMessageLike {
|
|
209
|
+
if (!Array.isArray(message.content)) {
|
|
210
|
+
return message;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const mapped = mapThinkingContentArray(
|
|
214
|
+
message.content,
|
|
215
|
+
normalizeThinkingLineForContext,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
return mapped.changed ? { ...message, content: mapped.content } : message;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function sanitizeContextMessages(messages: unknown): unknown {
|
|
222
|
+
if (!Array.isArray(messages)) {
|
|
223
|
+
return messages;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const messageList = messages as unknown[];
|
|
227
|
+
let changed = false;
|
|
228
|
+
const nextMessages = messageList.map((message) => {
|
|
229
|
+
if (!isRecord(message) || message.role !== "assistant") {
|
|
230
|
+
return message;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const sanitized = sanitizeThinkingBlocksForContext(message as AssistantMessageLike);
|
|
234
|
+
if (sanitized !== message) {
|
|
235
|
+
changed = true;
|
|
236
|
+
return sanitized;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return message;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return changed ? nextMessages : messageList;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function prefixThinkingBlocksForDisplay(
|
|
246
|
+
message: AssistantMessageLike,
|
|
247
|
+
theme: ThemeLike | undefined,
|
|
248
|
+
): void {
|
|
249
|
+
if (!shouldPrefixThinkingForApi(message.api)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const displayContent = withThinkingLabelsForDisplay(message.content, theme);
|
|
254
|
+
if (displayContent !== message.content) {
|
|
255
|
+
message.content = displayContent;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function extractAssistantMessage(event: unknown): AssistantMessageLike | undefined {
|
|
260
|
+
if (!isRecord(event)) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const maybeMessage = event.message;
|
|
265
|
+
if (!isRecord(maybeMessage)) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (maybeMessage.role !== "assistant") {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return maybeMessage as AssistantMessageLike;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function processThinkingEvent(
|
|
277
|
+
event: unknown,
|
|
278
|
+
ctx: ExtensionContext | undefined,
|
|
279
|
+
notifyPrefix: string,
|
|
280
|
+
): void {
|
|
281
|
+
try {
|
|
282
|
+
const message = extractAssistantMessage(event);
|
|
283
|
+
if (!message) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
prefixThinkingBlocksForDisplay(message, ctx?.ui?.theme);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
289
|
+
ctx?.ui?.notify(`${notifyPrefix}: ${errorMessage}`, "warning");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function handleThinkingMessageUpdateEvent(event: unknown, ctx: ExtensionContext | undefined): void {
|
|
294
|
+
// Render-only labeling: update the transient message_update payload while
|
|
295
|
+
// leaving canonical session/LLM context content untouched.
|
|
296
|
+
processThinkingEvent(event, ctx, "Thinking label formatting failed");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function handleThinkingMessageEndEvent(event: unknown, ctx: ExtensionContext | undefined): void {
|
|
300
|
+
// Persist themed labels on final assistant messages so the label remains
|
|
301
|
+
// visible after streaming ends and across session reloads.
|
|
302
|
+
// Context sanitization strips these presentation artifacts before each LLM call.
|
|
303
|
+
processThinkingEvent(event, ctx, "Thinking label finalization failed");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function handleThinkingContextEvent(event: unknown, ctx: ExtensionContext | undefined): void {
|
|
307
|
+
try {
|
|
308
|
+
if (!isRecord(event) || !Array.isArray(event.messages)) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const sanitizedMessages = sanitizeContextMessages(event.messages);
|
|
313
|
+
if (sanitizedMessages !== event.messages && Array.isArray(sanitizedMessages)) {
|
|
314
|
+
event.messages.splice(0, event.messages.length, ...(sanitizedMessages as unknown[]));
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
318
|
+
ctx?.ui?.notify(`Thinking context sanitization failed: ${message}`, "warning");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function registerThinkingLabeling(pi: ExtensionAPI): void {
|
|
323
|
+
if (registeredThinkingApis.has(pi)) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
registeredThinkingApis.add(pi);
|
|
327
|
+
|
|
328
|
+
onReloadShutdown(pi, () => {
|
|
329
|
+
registeredThinkingApis.delete(pi);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
pi.on("message_update", async (event, ctx) => {
|
|
333
|
+
handleThinkingMessageUpdateEvent(event, ctx);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
pi.on("message_end", async (event, ctx) => {
|
|
337
|
+
handleThinkingMessageEndEvent(event, ctx);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
pi.on("context", async (event, ctx) => {
|
|
341
|
+
handleThinkingContextEvent(event, ctx);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export interface PromptMetadata {
|
|
2
|
+
promptSnippet?: string;
|
|
3
|
+
promptGuidelines?: string[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const MCP_DESCRIPTION_PATTERN = /\bmcp\b/i;
|
|
7
|
+
const MCP_ADAPTER_SOURCE_PATTERN = /(?:^|[/\\@_-])(?:pi-)?mcp(?:[/\\@_-]|$)|pi-mcp-adapter|mcp-adapter/i;
|
|
8
|
+
const MAX_PROMPT_SNIPPET_LENGTH = 120;
|
|
9
|
+
|
|
10
|
+
export const MCP_PROXY_PROMPT_SNIPPET = "Discover, inspect, and call MCP tools across configured servers";
|
|
11
|
+
export const MCP_PROXY_PROMPT_GUIDELINES = [
|
|
12
|
+
"Use mcp for MCP discovery first: search by capability, describe one exact tool, then call it.",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function toRecord(value: unknown): Record<string, unknown> {
|
|
20
|
+
if (!isRecord(value)) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getTextField(value: unknown, field: string): string | undefined {
|
|
28
|
+
const record = toRecord(value);
|
|
29
|
+
const raw = record[field];
|
|
30
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeInlineText(value: string): string {
|
|
34
|
+
return value.trim().replace(/\s+/g, " ");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function trimPromptSnippet(value: string): string {
|
|
38
|
+
if (value.length <= MAX_PROMPT_SNIPPET_LENGTH) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const truncated = value.slice(0, MAX_PROMPT_SNIPPET_LENGTH).trimEnd();
|
|
43
|
+
return `${truncated.replace(/[\s.,;:!?-]+$/u, "")}…`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildPromptSnippetFromDescription(description: string | undefined, fallback: string): string {
|
|
47
|
+
const normalizedDescription = normalizeInlineText(description || "");
|
|
48
|
+
const normalizedFallback = normalizeInlineText(fallback);
|
|
49
|
+
const base = normalizedDescription || normalizedFallback;
|
|
50
|
+
const firstSentence = base.split(/(?<=[.!?])\s+/u, 1)[0] ?? base;
|
|
51
|
+
const withoutSentencePunctuation = firstSentence.replace(/[.!?]+$/u, "").trim();
|
|
52
|
+
return trimPromptSnippet(withoutSentencePunctuation || base);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function extractPromptMetadata(tool: unknown): PromptMetadata {
|
|
56
|
+
const source = toRecord(tool);
|
|
57
|
+
const promptSnippet =
|
|
58
|
+
typeof source.promptSnippet === "string" && source.promptSnippet.trim().length > 0
|
|
59
|
+
? source.promptSnippet
|
|
60
|
+
: undefined;
|
|
61
|
+
const promptGuidelines = Array.isArray(source.promptGuidelines)
|
|
62
|
+
? source.promptGuidelines.filter(
|
|
63
|
+
(guideline): guideline is string =>
|
|
64
|
+
typeof guideline === "string" && guideline.trim().length > 0,
|
|
65
|
+
)
|
|
66
|
+
: undefined;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
promptSnippet,
|
|
70
|
+
promptGuidelines:
|
|
71
|
+
promptGuidelines && promptGuidelines.length > 0
|
|
72
|
+
? [...promptGuidelines]
|
|
73
|
+
: undefined,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasMcpSourceInfo(value: unknown): boolean {
|
|
78
|
+
const sourceInfo = toRecord(value);
|
|
79
|
+
for (const [key, raw] of Object.entries(sourceInfo)) {
|
|
80
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const normalizedKey = key.toLowerCase();
|
|
85
|
+
const normalizedValue = raw.trim();
|
|
86
|
+
if (["source", "type", "kind", "origin"].includes(normalizedKey) && normalizedValue.toLowerCase() === "mcp") {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (MCP_ADAPTER_SOURCE_PATTERN.test(normalizedValue)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isMcpToolCandidate(tool: unknown): boolean {
|
|
98
|
+
if (!tool || typeof tool !== "object") {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const record = tool as Record<string, unknown>;
|
|
103
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
104
|
+
const description = typeof record.description === "string" ? record.description : "";
|
|
105
|
+
const label = typeof record.label === "string" ? record.label : "";
|
|
106
|
+
|
|
107
|
+
if (name === "mcp") {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (MCP_DESCRIPTION_PATTERN.test(description) || MCP_DESCRIPTION_PATTERN.test(label)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (hasMcpSourceInfo(record.sourceInfo)) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (/^mcp[_-]/i.test(name) || /_mcp$/i.test(name)) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (name.includes(":")) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (/^ctx_/i.test(name)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const params = record.parameters;
|
|
127
|
+
if (params && typeof params === "object") {
|
|
128
|
+
const parameterRecord = params as Record<string, unknown>;
|
|
129
|
+
if (
|
|
130
|
+
"mcpServer" in parameterRecord ||
|
|
131
|
+
"serverUrl" in parameterRecord ||
|
|
132
|
+
"server_name" in parameterRecord
|
|
133
|
+
) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return false;
|
|
139
|
+
}
|